Skip to content

Commit

Permalink
feat(notice-bar): update notice bar
Browse files Browse the repository at this point in the history
update notice bar style to v2, implementation of synchronized vue

feat Tencent#404
  • Loading branch information
slatejack committed Aug 27, 2024
1 parent 3acb9ef commit 7763a4d
Show file tree
Hide file tree
Showing 6 changed files with 185 additions and 157 deletions.
272 changes: 135 additions & 137 deletions src/notice-bar/NoticeBar.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
import { InfoCircleFilledIcon, CheckCircleFilledIcon, CloseCircleFilledIcon } from 'tdesign-icons-react';
import cls from 'classnames';
import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
import { InfoCircleFilledIcon, CheckCircleFilledIcon } from 'tdesign-icons-react';
import classNames from 'classnames';
import isObject from 'lodash/isObject';
import parseTNode from 'tdesign-mobile-react/_util/parseTNode';
import isArray from 'lodash/isArray';
import Swiper from 'tdesign-mobile-react/swiper';
import SwiperItem from 'tdesign-mobile-react/swiper/SwiperItem';
import { ConfigContext } from '../config-provider';
import type { StyledProps } from '../common';
import type { TdNoticeBarProps, NoticeBarTrigger } from './type';
import type { TdNoticeBarProps, NoticeBarTrigger, NoticeBarMarquee } from './type';
import useDefault from '../_util/useDefault';
import useDefaultProps from '../hooks/useDefaultProps';
import { noticeBarDefaultProps } from './defaultProps';
import noop from '../_util/noop';

export interface NoticeBarProps extends TdNoticeBarProps, StyledProps {}

Expand Down Expand Up @@ -44,24 +52,9 @@ const defaultIcons: Record<TdNoticeBarProps['theme'], IconType> = {
info: <InfoCircleFilledIcon />,
success: <CheckCircleFilledIcon />,
warning: <InfoCircleFilledIcon />,
error: <CloseCircleFilledIcon />,
error: <InfoCircleFilledIcon />,
};

function filterUndefinedValue<T extends Record<string, any>>(obj: T): Partial<T> {
const keys = Object.keys(obj);
const result = keys.reduce((prev, next: keyof T) => {
if (typeof obj[next] !== 'undefined') {
return {
...prev,
[next]: obj[next],
};
}
return prev;
}, {});

return result;
}

function useAnimationSettingValue() {
const animationSettingValue = useRef<frameState>(defaultReduceState());
const [, setState] = useState(0);
Expand Down Expand Up @@ -89,6 +82,7 @@ function useAnimationSettingValue() {
animationSettingValue.current = obj || defaultReduceState();
setState(Math.random());
}

return {
animationSettingValue,
updateScroll,
Expand All @@ -98,60 +92,48 @@ function useAnimationSettingValue() {
}

const NoticeBar: React.FC<NoticeBarProps> = (props) => {
const { classPrefix } = useContext(ConfigContext);
const {
content,
extra,
direction,
marquee,
operation,
prefixIcon,
suffixIcon,
theme = 'info',
visible,
defaultVisible,
onChange,
onClick,
} = props;
} = useDefaultProps(props, noticeBarDefaultProps);

const { animationSettingValue, updateScroll, updateAnimationFrame } = useAnimationSettingValue();

const name = `${classPrefix}-notice-bar`;
const listDOM = useRef<HTMLDivElement | null>(null);
const itemDOM = useRef<HTMLDivElement | null>(null);
const hasBeenExecute = useRef(false);

const showExtraText = !!extra;
const rootClasses = useMemo(() => cls([name, `${name}--${theme}`]), [name, theme]);
const { classPrefix } = useContext(ConfigContext);
const [isShow] = useDefault(visible, defaultVisible, noop);
const rootClassName = `${classPrefix}-notice-bar`;
const containerClassName = classNames(rootClassName, `${rootClassName}--${theme}`);
const { animationSettingValue, updateScroll, updateAnimationFrame } = useAnimationSettingValue();

const computedPrefixIcon: TdNoticeBarProps['prefixIcon'] | IconType | null = useMemo(() => {
let temp = null;
if (prefixIcon !== '') {
if (Object.keys(defaultIcons).includes(theme)) {
temp = defaultIcons[theme];
useEffect(() => {
if (!hasBeenExecute.current) {
if (isShow) {
hasBeenExecute.current = true;
handleScrolling();
}

return prefixIcon || temp || null;
return;
}
return null;
}, [prefixIcon, theme]);

const handleClick = useCallback(
(trigger: NoticeBarTrigger) => {
onClick?.(trigger);
},
[onClick],
);

const animateStyle = useMemo(
() => ({
transform: animationSettingValue.current.offset ? `translateX(${animationSettingValue.current.offset}px)` : '',
transitionDuration: `${animationSettingValue.current.duration}s`,
transitionTimingFunction: 'linear',
}),
setTimeout(() => {
if (isShow) {
updateAnimationFrame({
offset: animationSettingValue.current.listWidth,
duration: 0,
});
handleScrolling();
}
}, 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
[animationSettingValue.current.offset, animationSettingValue.current.duration],
);

const listDOM = useRef<HTMLDivElement | null>(null);
const itemDOM = useRef<HTMLDivElement | null>(null);

const [isShow] = useDefault(visible, defaultVisible, onChange);
}, [isShow]);

function handleScrolling() {
// 过滤 marquee 为 false
Expand All @@ -170,13 +152,16 @@ const NoticeBar: React.FC<NoticeBarProps> = (props) => {
updateScrollState = {
...animationSettingValue.current.scroll,
...defaultReduceState().scroll,
marquee: true,
marquee,
};
} else {
}
if (isObject(marquee)) {
const curMarquee = marquee as NoticeBarMarquee;
updateScrollState = {
...animationSettingValue.current.scroll,
...filterUndefinedValue(marquee),
marquee: true,
loop: typeof curMarquee?.loop === 'undefined' ? updateScrollState.loop : curMarquee.loop,
speed: curMarquee.speed ?? updateScrollState.speed,
delay: curMarquee.delay ?? updateScrollState.delay,
};
}

Expand Down Expand Up @@ -225,88 +210,101 @@ const NoticeBar: React.FC<NoticeBarProps> = (props) => {
}, 0);
}

const listScrollDomCls = cls(`${name}__list`, {
[`${name}__list--scrolling`]: animationSettingValue.current.scroll.marquee,
});

const listItemScrollDomCls = cls(`${name}__item`, {
[`${name}__item-detail`]: showExtraText,
});

const renderPrefixIcon = useMemo(
() =>
computedPrefixIcon ? (
<div className={`${name}__hd`} onClick={() => handleClick('prefix-icon')}>
{computedPrefixIcon}
</div>
) : null,
[handleClick, name, computedPrefixIcon],
const handleClick = (trigger: NoticeBarTrigger) => {
onClick?.(trigger);
};
// 动画
const animateStyle = useMemo(
() => ({
transform: animationSettingValue.current.offset ? `translateX(${animationSettingValue.current.offset}px)` : '',
transitionDuration: `${animationSettingValue.current.duration}s`,
transitionTimingFunction: 'linear',
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[animationSettingValue.current.offset, animationSettingValue.current.duration],
);

function onClickExtra(e: React.MouseEvent<HTMLSpanElement, MouseEvent>) {
e.stopPropagation();
handleClick('extra');
}

const itemDomStyle = animationSettingValue.current.scroll.marquee ? animateStyle : {};

const hasBeenExecute = useRef(false);

useEffect(() => {
if (!hasBeenExecute.current) {
if (isShow) {
hasBeenExecute.current = true;
handleScrolling();
}
return;
const renderPerfixIcon = () => {
const prefixIconContent = defaultIcons[theme || 'info'];
if (prefixIcon && prefixIconContent) {
return (
<div className={`${rootClassName}__prefix-icon`} onClick={() => handleClick('prefix-icon')}>
{prefixIconContent}
</div>
);
}
onChange?.(isShow);
setTimeout(() => {
if (isShow) {
updateAnimationFrame({
offset: animationSettingValue.current.listWidth,
duration: 0,
});
handleScrolling();
}
}, 0);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isShow]);

if (!isShow) {
return null;
}

return (
<div className={rootClasses}>
<div className={`${name}__inner`}>
{renderPrefixIcon}
<div className={`${name}__bd`}>
<div ref={listDOM} className={listScrollDomCls}>
<div
ref={itemDOM}
className={listItemScrollDomCls}
onTransitionEnd={handleTransitionend}
style={itemDomStyle}
>
<span className={`${name}__text`} onClick={() => handleClick('content')}>
{content}
{showExtraText && (
<span className={`${name}__text-detail`} onClick={onClickExtra}>
{extra}
</span>
)}
</span>
</div>
</div>
</div>
};

{suffixIcon && (
<div className={`${name}__ft`} onClick={() => handleClick('suffix-icon')}>
{suffixIcon}
const renderContent = () => {
const renderShowContent = () => parseTNode(content) || null;
const renderOperationContent = () => {
const operationContent = parseTNode(operation);
if (!operationContent) {
return null;
}
return (
<span
className={`${rootClassName}__operation`}
onClick={(e) => {
e.stopPropagation();
handleClick('operation');
}}
>
{operationContent}
</span>
);
};
return (
<div ref={listDOM} className={`${rootClassName}__content-wrap`} onClick={() => handleClick('content')}>
{direction === 'vertical' && isArray(content) ? (
<Swiper
className={`${rootClassName}__content--vertical`}
autoplay
loop
direction={direction}
duration={2000}
height={22}
>
{content.map((item, index) => (
<SwiperItem key={index}>
<div className={`${rootClassName}__content--vertical-item`}>{item}</div>
</SwiperItem>
))}
</Swiper>
) : (
<div
ref={itemDOM}
className={classNames(`${rootClassName}__content`, {
[`${rootClassName}__content-wrapable`]: animationSettingValue.current.scroll.marquee,
})}
style={animationSettingValue.current.scroll.marquee ? animateStyle : {}}
onTransitionEnd={handleTransitionend}
>
{renderShowContent()}
{renderOperationContent()}
</div>
)}
</div>
);
};

const renderSuffixIconContent = () => {
const suffixIconContent = parseTNode(suffixIcon);
if (!suffixIconContent) {
return null;
}
return (
<div className={`${rootClassName}__suffix-icon`} onClick={() => handleClick('suffix-icon')}>
{suffixIconContent}
</div>
);
};
return (
<div className={containerClassName}>
{renderPerfixIcon()}
{renderContent()}
{renderSuffixIconContent()}
</div>
);
};
Expand Down
7 changes: 6 additions & 1 deletion src/notice-bar/defaultProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@

import { TdNoticeBarProps } from './type';

export const noticeBarDefaultProps: TdNoticeBarProps = { marquee: false, theme: 'info', defaultVisible: false };
export const noticeBarDefaultProps: TdNoticeBarProps = {
direction: 'horizontal',
marquee: false,
theme: 'info',
defaultVisible: false,
};
22 changes: 22 additions & 0 deletions src/notice-bar/notice-bar.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
:: BASE_DOC ::

## API


### NoticeBar Props

name | type | default | description | required
-- | -- | -- | -- | --
className | String | - | className of component | N
style | Object | - | CSS(Cascading Style Sheets),Typescript:`React.CSSProperties` | N
content | TNode | - | Typescript:`string \| string[] \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
direction | String | horizontal | options: horizontal/vertical | N
marquee | Boolean / Object | false | Typescript:`boolean \| NoticeBarMarquee` `interface NoticeBarMarquee { speed?: number; loop?: number; delay?: number }`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/notice-bar/type.ts) | N
operation | TNode | - | Typescript:`string \| TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
prefixIcon | TElement | - | Typescript:`TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
suffixIcon | TElement | - | Typescript:`TNode`[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N
theme | String | info | options: info/success/warning/error | N
visible | Boolean | false | \- | N
defaultVisible | Boolean | false | uncontrolled property | N
onChange | Function | | Typescript:`(value: boolean) => void`<br/>`deprecated` | N
onClick | Function | | Typescript:`(trigger: NoticeBarTrigger) => void`<br/>[see more ts definition](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/notice-bar/type.ts)。<br/>`type NoticeBarTrigger = 'prefix-icon' \| 'content' \| 'operation' \| 'suffix-icon';`<br/> | N
Loading

0 comments on commit 7763a4d

Please sign in to comment.