Skip to content

Commit

Permalink
feat: 新增NoticeBar 通知栏组件
Browse files Browse the repository at this point in the history
  • Loading branch information
79E committed Dec 30, 2022
1 parent d8d8156 commit 4911985
Show file tree
Hide file tree
Showing 18 changed files with 496 additions and 0 deletions.
1 change: 1 addition & 0 deletions .umirc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ export default defineConfig({
'components/skeleton',
'components/segmented',
'components/water-mark',
'components/notice-bar',
],
},
{
Expand Down
64 changes: 64 additions & 0 deletions src/components/notice-bar/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# NoticeBar 通知栏

<code hidden="hidden" src="./demos/demo.tsx"></code>

## 介绍
适用于当前页面内信息的通知,是一种较醒目的页面内通知方式。

## 使用

```tsx
import { NoticeBar } from 'aunt';
```

### 基本用法
默认用法,文字长度超出容器后会自动进行滚动。
<code src="./demos/demo-base.tsx"></code>

### 自定义icon
可以对左侧右侧进行一些自定义的操作。
<code src="./demos/demo-icon.tsx"></code>

### 多行样式
可以通过`wrapable`属性进行多行展示,前提`scrollable``false`
<code src="./demos/demo-wrapable.tsx"></code>

### 滚动播放
文字长度超出后会自动进行滚动,也可将`scrollable`打开后多长都会进行滚动。
<code src="./demos/demo-scrollable.tsx"></code>

### 自定义颜色
可以通过`background` `color`进行一些颜色的自定义。
<code src="./demos/demo-color.tsx"></code>


## 参数

| 参数 | 说明 | 类型 | 默认值 |
| ----------- | --------- | ------- | ----- |
| closeable | 是否可关闭 | `boolean` | `false` |
| closeIcon | 自定义关闭图标 | `React.ReactNode` | `<AuntIconX />` |
| content | 通知文本内容 | `string \| React.ReactNode` | `-` |
| color | 通知文本颜色 | `string` | `#FF7D00` |
| background | 通知背景颜色 | `string` | `#FFF7E8` |
| delay | 动画延迟时间 (ms) | `number` | `0` |
| speed | 滚动速率 (px/s) | `number` | `1` |
| scrollable | 是否开启滚动播放,内容长度溢出时默认开启 | `boolean` | `false` |
| wrapable | 否开启文本换行,只在禁用滚动时生效 | `boolean` | `false` |
| leftIcon | 自定义左侧图标 | `React.ReactNode` | `-` |
| rightIcon | 自定义右侧图标 | `React.ReactNode` | `-` |
| onClose | 闭通知栏时触发 | `(event: React.MouseEvent) => void;` | `-` |
| onClick | 点击通知栏时触发 | `(event: React.MouseEvent) => void;` | `-` |
| onReplay | 每当滚动栏重新开始滚动时触发 | `() => void;` | `-` |

## 样式变量

| 属性名 | 说明 | 默认值 |
| --------- | ---------- | ----------------- |
| --aunt-notice-bar-background-color | 通知背景颜色 | `var(--aunt-orange-1);` |
| --aunt-notice-bar-color | 通知内容文字颜色 | `var(--aunt-orange);` |
| --aunt-notice-bar-font-size | 通知内容文字大小 | `var(--aunt-font-size-md);` |
| --aunt-notice-bar-padding | 通知内边距 | `var(--aunt-padding-xs);` |
| --aunt-notice-bar-item-margin | 左右两侧icon外边距 | `var(--aunt-padding-xs);` |


4 changes: 4 additions & 0 deletions src/components/notice-bar/demos/demo-base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import React from 'react';
import { NoticeBar } from 'aunt';

export default () => <NoticeBar content='这是一种通知的方式~' />;
21 changes: 21 additions & 0 deletions src/components/notice-bar/demos/demo-color.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';
import { NoticeBar, AuntIconPocket } from 'aunt';

export default () => (
<NoticeBar
color='rgb(0, 180, 42)'
background='rgb(232, 255, 234)'
leftIcon={<AuntIconPocket size={20} />}
content='如果不喜欢这个颜色,自己还可以自定义下颜色。'
closeable
onClick={() => {
console.log('点击了');
}}
onClose={() => {
console.log('关闭了');
}}
onReplay={() => {
console.log('进行重新开始了');
}}
/>
);
39 changes: 39 additions & 0 deletions src/components/notice-bar/demos/demo-icon.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React from 'react';
import { NoticeBar, Space, AuntIconBell, AuntIconXCircle, Toast } from 'aunt';

export default () => {
return (
<Space direction='vertical' style={{ width: '100%' }}>
<NoticeBar leftIcon={<AuntIconBell size={20} />} content='自定义了一下左侧的图标。' />
<NoticeBar closeable content='打开了右侧的关闭按钮。' />
<NoticeBar
closeable
closeIcon={<AuntIconXCircle size={20} />}
content='自定义了一下右侧的关闭图标。'
/>
<NoticeBar
rightIcon={
<div
style={{
width: 50,
height: 20,
background: '#ff7d00',
borderRadius: 20,
color: '#ffffff',
fontSize: 12,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
onClick={() => {
Toast('点我干啥~');
}}
>
点我
</div>
}
content='自定义了一下右侧的图标。'
/>
</Space>
);
};
6 changes: 6 additions & 0 deletions src/components/notice-bar/demos/demo-scrollable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React from 'react';
import { NoticeBar } from 'aunt';

export default () => {
return <NoticeBar content='这样文字如果够多的情况下,会自动开启滚动效果的。' closeable />;
};
12 changes: 12 additions & 0 deletions src/components/notice-bar/demos/demo-wrapable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import React from 'react';
import { NoticeBar } from 'aunt';

export default () => {
return (
<NoticeBar
wrapable
content='如果文字比较多的情况下也可以开启多行样式这样不会进行滚动的~'
closeable
/>
);
};
32 changes: 32 additions & 0 deletions src/components/notice-bar/demos/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import React from 'react';
import { DemoBlock } from 'demos';
import DemoBase from './demo-base';
import DemoIcon from './demo-icon';
import DemoWrapable from './demo-wrapable';
import DemoScrollable from './demo-scrollable';
import DemoColor from './demo-color';
import './index.less';

function Demo() {
return (
<div className='demo'>
<DemoBlock title='基础用法'>
<DemoBase />
</DemoBlock>
<DemoBlock title='自定义icon'>
<DemoIcon />
</DemoBlock>
<DemoBlock title='多行样式'>
<DemoWrapable />
</DemoBlock>
<DemoBlock title='滚动播放'>
<DemoScrollable />
</DemoBlock>
<DemoBlock title='自定义颜色'>
<DemoColor />
</DemoBlock>
</div>
);
}

export default Demo;
3 changes: 3 additions & 0 deletions src/components/notice-bar/demos/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.demo {

}
7 changes: 7 additions & 0 deletions src/components/notice-bar/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import './styles/index.less';
import { NoticeBar } from './notice-bar';

export type { NoticeBarProps } from './types';

export { NoticeBar };
export default NoticeBar;
146 changes: 146 additions & 0 deletions src/components/notice-bar/notice-bar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import React, {
useMemo,
useRef,
FunctionComponent,
useContext,
useLayoutEffect,
useCallback,
useState,
} from 'react';
import { AuntIconX } from '../icon/icons';
import ConfigProviderContext from '../config-provider/config-provider-context';
import { useNamespace, useTimeout } from '../../hooks';
import { joinTrim } from '../../utils';
import type { NoticeBarProps } from './types';

export const NoticeBar: FunctionComponent<NoticeBarProps> = props => {
const { prefix } = useContext(ConfigProviderContext);
const ns = useNamespace('notice-bar', prefix);

const { startTimer } = useTimeout();
const animationTimer = useRef<number>();

const [visible, setVisible] = useState(true);

const { content, speed = 1 } = props;

const wrapRef = useRef<HTMLDivElement | null>(null);
const contentRef = useRef<HTMLParagraphElement | null>(null);
const elWidth = useRef({
wrap: 0,
content: 0,
});

const varStyles = useMemo(() => {
const styles: React.CSSProperties = {};
if (props.color) styles.color = props.color;
if (props.background) styles.backgroundColor = props.background;
return { ...styles, ...props.style };
}, [props.color, props.background]);

const renderLeft = () => {
if (!React.isValidElement(props.leftIcon)) return null;
return <div className={ns.m('left')}>{props.leftIcon}</div>;
};

const renderRight = () => {
const element = (e: React.ReactNode) => <div className={ns.m('right')}>{e}</div>;
if (props.closeable) {
const size = contentRef.current?.clientHeight || 20;
const node = React.cloneElement(
React.isValidElement(props.closeIcon) ? props.closeIcon : <AuntIconX size={size} />,
{
onClick: (e: React.MouseEvent) => {
e.stopPropagation();
setVisible(false);
props.onClose?.(e);
},
}
);
return element(node);
}
if (React.isValidElement(props.rightIcon)) return element(props.rightIcon);
return null;
};

// 判断是否可以进行滚动
const isScrollable = useCallback(() => {
if (!wrapRef.current || !contentRef.current) return false;
if (!props.scrollable && props.wrapable) return false;
const is = wrapRef.current!.clientWidth >= contentRef.current!.clientWidth;
if (!props.scrollable && is) return false;
return true;
}, [wrapRef, contentRef]);

const getOffsetX = (ref: React.MutableRefObject<HTMLParagraphElement | null>) => {
const { transform } = ref.current!.style;
const ret = transform.split('px')[0].split('(')[1] || 0;
return ret;
};

const setOffsetX = (
num: string | number,
ref: React.MutableRefObject<HTMLParagraphElement | null>
) => {
ref.current!.style.transform = `translateX(${num}px)`;
};

// 清除动画
const clearAnimationFrame = () => {
if (animationTimer.current) window.cancelAnimationFrame(animationTimer.current);
};

// 移动方法
const move = (endPosition: number) => {
const contentOffsetX = getOffsetX(contentRef);
// 当移动到最左边后 把位置改到右边去 开启新一轮的循环
if (contentOffsetX <= endPosition) {
props.onReplay?.();
clearAnimationFrame();
setOffsetX(elWidth.current.wrap, contentRef);
move(endPosition);
return;
}
setOffsetX(Number(contentOffsetX) - speed, contentRef);
animationTimer.current = window.requestAnimationFrame(() => {
move(endPosition);
});
};

// 开始执行动画
const startMove = () => {
elWidth.current = {
wrap: wrapRef.current!.offsetWidth || 0,
content: contentRef.current!.offsetWidth || 0,
};

animationTimer.current = window.requestAnimationFrame(() => {
move(-elWidth.current.content);
});
};

useLayoutEffect(() => {
if (isScrollable() && visible) startTimer(startMove, props.delay);
return () => {
clearAnimationFrame();
};
}, [props, wrapRef, contentRef, visible]);

if (!visible) return null;

return (
<div className={joinTrim([ns.b(), props.className])} style={varStyles} onClick={props.onClick}>
{renderLeft()}
<div
className={ns.e('box')}
ref={wrapRef}
style={{
whiteSpace: !isScrollable() && props.wrapable ? 'normal' : 'nowrap',
}}
>
<p ref={contentRef}>{content}</p>
</div>
{renderRight()}
</div>
);
};
34 changes: 34 additions & 0 deletions src/components/notice-bar/styles/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
@class-prefix: ~'aunt';
:root{
--aunt-notice-bar-background-color: var(--aunt-orange-1);
--aunt-notice-bar-color: var(--aunt-orange);
--aunt-notice-bar-font-size: var(--aunt-font-size-md);
--aunt-notice-bar-padding: var(--aunt-padding-xs);
--aunt-notice-bar-item-margin: var(--aunt-padding-xs);
}

.@{class-prefix}-notice-bar {
padding: var(--aunt-notice-bar-padding);
display: flex;
align-items: flex-start;
background-color: var(--aunt-notice-bar-background-color);
color: var(--aunt-notice-bar-color);
font-size: var(--aunt-notice-bar-font-size);

&--left{
margin-right: var(--aunt-notice-bar-item-margin);
}
&--right{
margin-left: var(--aunt-notice-bar-item-margin);
}
&__box{
width: 100%;
height: 100%;
overflow: hidden;
word-break: break-all;
p{
display: inline-block;
transition-timing-function: linear;
}
}
}
5 changes: 5 additions & 0 deletions src/components/notice-bar/tests/index.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import '@testing-library/jest-dom';
import React from 'react';
import { render, screen } from '@testing-library/react';

describe('< />', () => {});
Loading

0 comments on commit 4911985

Please sign in to comment.