diff --git a/.gitignore b/.gitignore index fc0e60d..8f23f58 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /node_modules *.bak /site -yarn-error.log \ No newline at end of file +yarn-error.log +**/**/.DS_Store diff --git a/components/notice-bar/README.md b/components/notice-bar/README.md index 945ad03..088b695 100644 --- a/components/notice-bar/README.md +++ b/components/notice-bar/README.md @@ -2,7 +2,7 @@ ### Install -```js; +```js import { NoticeBar } from '@trillion/muld'; ``` @@ -22,7 +22,7 @@ import { NoticeBar } from '@trillion/muld'; ``` @@ -30,7 +30,7 @@ import { NoticeBar } from '@trillion/muld'; ### Wrapable ```html - + Notice Content ``` @@ -50,12 +50,11 @@ import { NoticeBar } from '@trillion/muld'; ### Custom Style ```html - + Notice Content ``` - ## API ### Props @@ -66,9 +65,9 @@ import { NoticeBar } from '@trillion/muld'; | text | Notice text content | _string_ | `''` | - | | color | Text color | _string_ | `#f60` | | background | Background color | _string_ | `#fff7cc` | -| left-icon | Left Icon | _string_ | - | -| delay | Animation delay (s) | _number \| string_ | `1` | -| speed | Scroll speed (px/s) | _number \| string_ | `50` | +| leftIcon | Left Icon | _string_ | - | +| delay | Animation delay (s) | _number_ | `1` | +| speed | Scroll speed (px/s) | _number_ | `50` | | scrollable | Whether to scroll content | _boolean_ | - | | wrapable | Whether to enable text wrap | _boolean_ | `false` | - | @@ -76,14 +75,6 @@ import { NoticeBar } from '@trillion/muld'; | Event | Description | Arguments | | --------------- | ------------------------------ | -------------- | -| click | Triggered when click NoticeBar | _event: Event_ | -| close | Triggered when closed | _event: Event_ | -| replay `v2.6.2` | Triggered when replay | - | - -### Slots - -| Name | Description | -| ---------- | ------------------- | -| default | Notice text content | -| left-icon | Custom left icon | -| right-icon | Custom right icon | +| onClick | Triggered when click NoticeBar | _event: React.MouseEvent__ | +| onClose | Triggered when closed | _event: React.MouseEvent__ | +| onReplay | Triggered when replay | - | diff --git a/components/notice-bar/README.zh-CN.md b/components/notice-bar/README.zh-CN.md index ce03563..19e3459 100644 --- a/components/notice-bar/README.zh-CN.md +++ b/components/notice-bar/README.zh-CN.md @@ -1,3 +1,4 @@ + # NoticeBar 通知栏 ### 引入 @@ -82,9 +83,9 @@ import { NoticeBar } from '@trillion/muld'; | text | 通知文本内容 | _string_ | `''` | | color | 通知文本颜色 | _string_ | `#f60` | | background | 滚动条背景 | _string_ | `#fff7cc` | -| left-icon | 左侧[图标名称](#/zh-CN/icon)或图片链接 | _string_ | - | -| delay | 动画延迟时间 (s) | _number \| string_ | `1` | -| speed | 滚动速率 (px/s) | _number \| string_ | `50` | +| leftIcon | 左侧[图标名称](#/zh-CN/icon) | _string_ | - | +| delay | 动画延迟时间 (s) | _number_ | `1` | +| speed | 滚动速率 (px/s) | _number_ | `50` | | scrollable | 是否开启滚动播放,内容长度溢出时默认开启 | _boolean_ | - | | wrapable | 是否开启文本换行,只在禁用滚动时生效 | _boolean_ | `false` | @@ -92,14 +93,7 @@ import { NoticeBar } from '@trillion/muld'; | 事件名 | 说明 | 回调参数 | | --------------- | ---------------------------- | -------------- | -| click | 点击通知栏时触发 | _event: Event_ | -| close | 关闭通知栏时触发 | _event: Event_ | -| replay `v2.6.2` | 每当滚动栏重新开始滚动时触发 | - | - -### Slots +| onClick | 点击通知栏时触发 | _event: React.MouseEvent_| +| onClose | 关闭通知栏时触发 | _event: React.MouseEvent_ | +| onReplay | 每当滚动栏重新开始滚动时触发 | - | -| 名称 | 内容 | -| ---------- | -------------- | -| default | 通知文本内容 | -| left-icon | 自定义左侧图标 | -| right-icon | 自定义右侧图标 | diff --git a/components/notice-bar/demo/index.tsx b/components/notice-bar/demo/index.tsx new file mode 100644 index 0000000..996ba8d --- /dev/null +++ b/components/notice-bar/demo/index.tsx @@ -0,0 +1,50 @@ +import * as React from 'react'; +import DemoSection from '@trillion/muld-tools/site/mobile/layout/DemoSection'; +import DemoBlock from '@trillion/muld-tools/site/mobile/layout/DemoBlock'; +import styled from 'styled-components'; +import NoticeBar from '..'; +import { $padding_base } from '../../style/var'; + +export default function NoticeBarDemo() { + function onClose() { + console.log('closed!!'); + } + const longText = '在代码阅读过程中人们说脏话的频率是衡量代码质量的唯一标准。'; + const shortText = '技术是开发它的人的共同灵魂。'; + return ( + + + + + +
+ +
+ +
+ + + + +
+ + {shortText} + +
+ +
+ + + +
+ ); +} + +const View = styled(DemoSection)` + &.demo-notice-bar { + background: #fff; + .demo-row { + margin-bottom: ${$padding_base}; + } + } +`; diff --git a/components/notice-bar/index.tsx b/components/notice-bar/index.tsx index 87a0db4..52ec5c5 100644 --- a/components/notice-bar/index.tsx +++ b/components/notice-bar/index.tsx @@ -1,164 +1,136 @@ -/* eslint-disable */ import * as React from 'react'; import classnames from 'classnames'; -import { createNS, withDefaultProps, isDef } from '../utils'; -import { doubleRaf } from '../utils/dom/raf'; +import { createNS, withDefaultProps } from '../utils'; import { View } from './style'; +import Icon from '../icon'; +const [bem] = createNS('notice-bar'); +const { useState, useEffect, useRef } = React; -interface Props extends React.HTMLAttributes { - text: string; +export interface NoticeBarProps { + text?: string; mode?: string; + scrollable?: boolean | void; color?: string; - leftIcon?: string | React.ReactNode; - rightIcon?: string | React.ReactNode; - wrapable?: boolean; + leftIcon?: string; background?: string; - scrollable?: boolean; - delay?: number | string; - speed?: number | string; - onClick?: (event: React.MouseEvent) => void; - onClose?: (event: React.MouseEvent) => void; + wrapable: boolean; + delay: number; + speed: number; + children?: React.ReactNode; + onClose?: (event?: React.MouseEvent) => void; + onClick?: (event?: React.MouseEvent) => void; + onReplay?: () => void; } + const defaultProps = { - scrollable: false, delay: 1, speed: 50, + wrapable: false, }; -export type NoticeBarProps = Props & typeof defaultProps; -const [bem] = createNS('card'); -const { useState, useRef } = React; -const NoticeBar: React.FC> = (props: NoticeBarProps) => { +const NoticeBar: React.FC = (props) => { const { text, - mode, - color, - // leftIcon, - wrapable, - background, + children, scrollable, - speed, + wrapable, + mode, onClose, + onReplay, onClick, - // rightIcon, + leftIcon, + speed, + delay, + color, + background, } = props; const [show, setShow] = useState(true); - const [panel, setPanel] = useState({ - offset: 0, - duration: 0, - wrapWidth: 0, + const contentRef = useRef(null); + const wrapperRef = useRef(null); + const rectInfo = useRef({ + wrapperWidth: 0, contentWidth: 0, - }); - const wrapRef = useRef(null); - const contentRef = useRef(null); - - // function onClickIcon(event: React.MouseEvent) { - // if (mode === 'closeable') { - // setShow(false); - // onClose && onClose(event); - // } - // } - - // TODO: - // function onTransitionEnd() {} - - // function reset() { - // setPanel({ - // offset: 0, - // duration: 0, - // wrapWidth: 0, - // contentWidth: 0, - // }); - // } - - // function start() { - // const delay = isDef(props.delay) ? props.delay * 1000 : 0; - - // reset(); - - // setTimeout(() => { - // if (!wrapRef || !contentRef || scrollable === false) { - // return; - // } - - // const wrapWidth = (wrapRef as any).current.getBoundingClientRect().width; - // const contentWidth = (contentRef as any).current.getBoundingClientRect().width; - - // if (scrollable || contentWidth > wrapWidth) { - // doubleRaf(() => { - // setPanel({ - // offset: -contentWidth, - // duration: contentWidth / speed, - // wrapWidth, - // contentWidth, - // }); - // }); - // } - // }, delay); - // } - - const barStyle = { - color, - background, - display: show ? 'block' : 'none', - }; - - const contentStyle = { - transform: panel.offset ? `translateX(${panel.offset}px)` : '', - transitionDuration: `${panel.duration}s`, - }; - - // TODO: use icon - // function LeftIcon() { - // if (typeof leftIcon === 'string') { - // return ; - // } - // return leftIcon; - // } - - // TODO: use icon - // function RightIcon() { - // if (typeof rightIcon === 'string') { - // let iconName; - // if (mode === 'closeable') { - // iconName = 'cross'; - // } else if (mode === 'link') { - // iconName = 'arrow'; - // } - - // if (iconName) { - // return ; - // } - // } - // return rightIcon; - // } - - return ( + }).current; + + function setContentStyle(offset: number, duration: number) { + contentRef.current!.style.transform = `translateX(${offset}px)`; + contentRef.current!.style.transitionDuration = `${duration}s`; + } + + useEffect(() => { + rectInfo.wrapperWidth = wrapperRef.current!.getBoundingClientRect().width; + rectInfo.contentWidth = contentRef.current!.getBoundingClientRect().width; + const wrapperWidth = rectInfo.wrapperWidth; + const contentWidth = rectInfo.contentWidth; + if (scrollable === false) { + return; + } + if (scrollable === undefined && contentWidth <= wrapperWidth) { + return; + } + setTimeout(() => { + setContentStyle(-contentWidth, contentWidth / speed); + }, delay * 1000); + }, []); + + const contentClasses = [bem('content'), { 'mul-ellipsis': scrollable === false && !wrapable }]; + const wrapperClasses = [bem({ wrapable })]; + + function onRightIconClick(e: React.MouseEvent) { + setShow(false); + onClose && onClose(e); + } + + function RightIcon() { + if (mode === 'closeable') { + return ( + + ); + } + if (mode === 'link') { + return ; + } + return null; + } + + function LeftIcon() { + return leftIcon ? : null; + } + + function onTransitionEnd() { + const { wrapperWidth, contentWidth } = rectInfo; + setContentStyle(wrapperWidth, 0); + contentRef.current!.offsetLeft; // 主动触发重排 + setContentStyle(-contentWidth, (contentWidth + wrapperWidth) / speed); + onReplay && onReplay(); + } + + return show ? ( { - onClick && onClick(event); - }} + className={classnames(wrapperClasses)} + onClick={onClick} + color={color} + background={background} > - {/* {LeftIcon()} */} -
+ +
- {props.children || text} + {text || children}
- {/* {RightIcon()} */} + - ); + ) : null; }; -export default withDefaultProps(React.memo(NoticeBar), defaultProps); +export default withDefaultProps(NoticeBar, defaultProps); diff --git a/components/notice-bar/style.tsx b/components/notice-bar/style.tsx index 796984a..eb1049f 100644 --- a/components/notice-bar/style.tsx +++ b/components/notice-bar/style.tsx @@ -1,30 +1,36 @@ import styled from 'styled-components'; import { $notice_bar } from '../style/var'; +import { ellipsis } from '../style/mixins/ellipsis'; -export const View = styled.div` +type ViewProps = { + background?: string; + color?: string; +}; + +export const View = styled.div` &.mul-notice-bar { position: relative; display: flex; align-items: center; height: ${$notice_bar.height}; padding: ${$notice_bar.padding}; - color: ${$notice_bar.text_color}; + color: ${(props: ViewProps) => props.color || $notice_bar.text_color}; font-size: ${$notice_bar.font_size}; line-height: ${$notice_bar.line_height}; - background-color: ${$notice_bar.background_color}; + background-color: ${(props: ViewProps) => props.background || $notice_bar.background_color}; - &__left-icon, - &__right-icon { + .mul-notice-bar__left-icon, + .mul-notice-bar__right-icon { min-width: ${$notice_bar.icon_min_width}; font-size: ${$notice_bar.icon_size}; } - &__right-icon { + .mul-notice-bar__right-icon { text-align: right; cursor: pointer; } - &__wrap { + .mul-notice-bar__wrap { position: relative; display: flex; flex: 1; @@ -33,30 +39,28 @@ export const View = styled.div` overflow: hidden; } - &__content { + .mul-notice-bar__content { position: absolute; white-space: nowrap; transition-timing-function: linear; &.mul-ellipsis { max-width: 100%; + ${ellipsis()} } } &--wrapable { height: auto; padding: ${$notice_bar.wrapable_padding}; + .mul-notice-bar__wrap { + height: auto; + } - .mul-notice-bar { - &__wrap { - height: auto; - } - - &__content { - position: relative; - white-space: normal; - word-wrap: break-word; - } + .mul-notice-bar__content { + position: relative; + white-space: normal; + word-wrap: break-word; } } } diff --git a/components/notice-bar/test/index.test.tsx b/components/notice-bar/test/index.test.tsx new file mode 100644 index 0000000..72de795 --- /dev/null +++ b/components/notice-bar/test/index.test.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import '@testing-library/jest-dom/extend-expect'; +import { render, cleanup, fireEvent } from '@testing-library/react'; +import 'regenerator-runtime'; +import NoticeBar from '../index'; + +const EventDemo = (props: any) => { + const [text, setText] = React.useState(''); + const onClick = () => { + setText('clicked'); + }; + const onReplay = () => { + setText('transitionend'); + }; + return ( + + {text} + + ); +}; + +describe('NoticeBar', () => { + afterEach(cleanup); + it('click event valid', () => { + const { container } = render(); + const content = container.querySelector('.mul-notice-bar__content'); + fireEvent.click(content!); + expect(content?.textContent).toBe('clicked'); + }); + it('close event valid', () => { + const { container } = render(); + const rightIcon = container.querySelector('.mul-notice-bar__right-icon'); + + fireEvent.click(rightIcon!); + expect(container).toBeEmpty(); + }); + it('replay event valid', () => { + const { container } = render(); + const content = container.querySelector('.mul-notice-bar__content'); + fireEvent.transitionEnd(content!); + expect(content?.textContent).toBe('transitionend'); + }); +}); diff --git a/components/style/var.ts b/components/style/var.ts index c8550dc..cc3348b 100644 --- a/components/style/var.ts +++ b/components/style/var.ts @@ -219,15 +219,15 @@ export const $nav_bar = { // NoticeBar export const $notice_bar = { - height: `40px`, + height: '40px', padding: `0 ${$padding_md}`, wrapable_padding: `${$padding_xs} ${$padding_md}`, text_color: `${$orange_dark}`, font_size: `${$font_size_md}`, - line_height: `24px`, + line_height: '24px', background_color: `${$orange_light}`, - icon_size: `16px`, - icon_min_width: `24px`, + icon_size: '16px', + icon_min_width: '24px', }; // Notify