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(Overlay): complete refactoring #451

Merged
merged 6 commits into from
Aug 12, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 6 additions & 1 deletion site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export default {
name: 'image',
component: () => import('tdesign-mobile-react/image/_example/index.jsx'),
},
{
title: 'Overlay 遮罩层',
name: 'overlay',
component: () => import('tdesign-mobile-react/overlay/_example/index.tsx'),
},
{
title: 'Popup 弹出层',
name: 'popup',
Expand Down Expand Up @@ -216,6 +221,6 @@ export default {
title: 'Result 结果',
name: 'result',
component: () => import('tdesign-mobile-react/result/_example/index.tsx'),
}
},
],
};
6 changes: 6 additions & 0 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,12 @@ export default {
path: '/mobile-react/components/message',
component: () => import('tdesign-mobile-react/message/message.md'),
},
{
title: 'Overlay 弹出层',
name: 'overlay',
path: '/mobile-react/components/overlay',
component: () => import('tdesign-mobile-react/overlay/overlay.md'),
},
{
title: 'Popup 弹出层',
name: 'popup',
Expand Down
39 changes: 39 additions & 0 deletions src/_util/parseTNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { ReactElement, ReactNode } from 'react';
import isFunction from 'lodash/isFunction';
import { TNode } from '../common';
import log from '../_common/js/log';

// 解析 TNode 数据结构
export default function parseTNode(
renderNode: TNode | TNode<any> | undefined,
renderParams?: any,
defaultNode?: ReactNode,
): ReactNode {
let node: ReactNode = null;

if (typeof renderNode === 'function') {
node = renderNode(renderParams);
} else if (renderNode === true) {
node = defaultNode;
} else if (renderNode !== null) {
node = renderNode ?? defaultNode;
}
return node as ReactNode;
}

/**
* 解析各种数据类型的 TNode
* 函数类型:content={(props) => <Icon></Icon>}
* 组件类型:content={<Button>click me</Button>} 这种方式可以避免函数重复渲染,对应的 props 已经注入
* 字符类型
*/
export function parseContentTNode<T>(tnode: TNode<T>, props: T) {
if (isFunction(tnode)) return tnode(props) as ReactNode;
if (!tnode || ['string', 'number', 'boolean'].includes(typeof tnode)) return tnode as ReactNode;
try {
return React.cloneElement(tnode as ReactElement, { ...props });
} catch (e) {
log.warn('parseContentTNode', `${tnode} is not a valid ReactNode`);
return null;
}
}
2 changes: 1 addition & 1 deletion src/dialog/Dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ export const Dialog: React.FC<DialogProps> = (props) => {
>
{showOverlay ? (
<animated.div style={maskSpring}>
<Overlay onOverlayClick={onOverlayClickHandle} disableBodyScroll={false} />
<Overlay visible={visible} onClick={onOverlayClickHandle} />
</animated.div>
) : null}
<div className="wrap" style={wrapStyle}>
Expand Down
18 changes: 4 additions & 14 deletions src/drawer/Drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,8 @@ enum PopupSourceEnum {
}

const Drawer: React.FC<TdDrawerProps> = (props) => {
const {
items,
visible,
showOverlay,
zIndex,
closeOnOverlayClick,
destroyOnClose,
placement,
onClose,
onItemClick,
onOverlayClick,
} = props;
const { items, visible, showOverlay, zIndex, closeOnOverlayClick, placement, onClose, onItemClick, onOverlayClick } =
props;

const { classPrefix } = useConfig();
const name = `${classPrefix}-drawer`;
Expand All @@ -34,7 +24,7 @@ const Drawer: React.FC<TdDrawerProps> = (props) => {

const [show, setShow] = useState(visible);

const handleOverlayClick = (e: React.MouseEvent<HTMLDivElement>) => {
const handleOverlayClick = (e) => {
const context = { e };
onOverlayClick?.(context);
if (closeOnOverlayClick) {
Expand All @@ -57,9 +47,9 @@ const Drawer: React.FC<TdDrawerProps> = (props) => {
<Popup
visible={show}
placement={placement}
overlayProps={{ onOverlayClick: handleOverlayClick, destroyOnClose }}
showOverlay={showOverlay}
zIndex={zIndex}
onVisibleChange={handleOverlayClick}
>
<div className={`${name}__sidebar`}>
{items?.map((item, index) => {
Expand Down
8 changes: 8 additions & 0 deletions src/hooks/useClass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { useMemo } from 'react';
import useConfig from './useConfig';

export function usePrefixClass(componentName?: string) {
const { classPrefix } = useConfig();

return useMemo(() => (componentName ? `${classPrefix}-${componentName}` : classPrefix), [classPrefix, componentName]);
}
4 changes: 4 additions & 0 deletions src/hooks/useConfig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { useContext } from 'react';
import ConfigContext from '../config-provider/ConfigContext';

export default () => useContext(ConfigContext);
12 changes: 5 additions & 7 deletions src/_util/useLockScroll.ts → src/hooks/useLockScroll.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { useEffect, RefObject, useCallback } from 'react';
import { useTouch } from './useTouch';
import getScrollParent from './getScrollParent';
import { supportsPassive } from './supportsPassive';
import useConfig from './useConfig';
import { useTouch } from '../_util/useTouch';
import getScrollParent from '../_util/getScrollParent';
import { supportsPassive } from '../_util/supportsPassive';

let totalLockCount = 0;

// 移植自vant:https://github.com/youzan/vant/blob/HEAD/src/composables/use-lock-scroll.ts
export function useLockScroll(rootRef: RefObject<HTMLElement>, shouldLock: boolean) {
export function useLockScroll(rootRef: RefObject<HTMLElement>, shouldLock: boolean, componentName: string) {
const touch = useTouch();
const { classPrefix } = useConfig();
const BODY_LOCK_CLASS = `${classPrefix}-overflow-hidden`;
const BODY_LOCK_CLASS = `${componentName}-overflow-hidden`;

const onTouchMove = useCallback(
(event: TouchEvent) => {
Expand Down
169 changes: 69 additions & 100 deletions src/overlay/Overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,114 +1,83 @@
import React, { FC, useRef, useMemo, useState } from 'react';
import { useUnmountedRef } from 'ahooks';
import { useSpring, animated } from '@react-spring/web';
import withNativeProps, { NativeProps } from '../_util/withNativeProps';
import { PropagationEvent, withStopPropagation } from '../_util/withStopPropagation';
import { GetContainer, renderToContainer } from '../_util/renderToContainer';
import { useLockScroll } from '../_util/useLockScroll';
import { useShouldRender } from '../_util/useShouldRender';
import useConfig from '../_util/useConfig';
import React, { forwardRef, useEffect, useRef, useMemo, useState } from 'react';
import classNames from 'classnames';
import { CSSTransition } from 'react-transition-group';
import { StyledProps } from '../common';
import { TdOverlayProps } from './type';
import { overlayDefaultProps } from './defaultProps';
import { useLockScroll } from '../hooks/useLockScroll';
import parseTNode from '../_util/parseTNode';
import useDefaultProps from '../hooks/useDefaultProps';
import { usePrefixClass } from '../hooks/useClass';

export interface OverlayProps extends NativeProps {
visible?: boolean;
onOverlayClick?: (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => void;
destroyOnClose?: boolean;
forceRender?: boolean;
disableBodyScroll?: boolean;
color?: 'black' | 'white';
opacity?: 'default' | 'thin' | 'thick' | number;
getContainer?: GetContainer;
afterShow?: () => void;
afterClose?: () => void;
stopPropagation?: PropagationEvent[];
children?: React.ReactNode;
}
export interface OverlayProps extends TdOverlayProps, StyledProps {}

const opacityRecord = {
default: 0.55,
thin: 0.35,
thick: 0.75,
};
const Overlay = forwardRef<HTMLDivElement, OverlayProps>((props) => {
const overlayClass = usePrefixClass('overlay');
const maskRef = useRef<HTMLDivElement>();

const defaultProps = {
visible: true,
destroyOnClose: false,
forceRender: false,
color: 'black',
opacity: 'default',
disableBodyScroll: true,
getContainer: null,
stopPropagation: ['click'],
} as OverlayProps;
const { className, style, backgroundColor, children, duration, preventScrollThrough, visible, zIndex, onClick } =
useDefaultProps<OverlayProps>(props, overlayDefaultProps);

const Overlay: FC<OverlayProps> = (props) => {
const ref = useRef<HTMLDivElement>(null);
useLockScroll(ref, props.visible && props.disableBodyScroll);
const { classPrefix } = useConfig();
const name = `${classPrefix}-overlay`;
const [shouldRender, setShouldRender] = useState(visible); // 确保 CSSTransition 只在 visible 变为 true 时渲染

const background = useMemo(() => {
const opacity = opacityRecord[props.opacity] ?? props.opacity;
const rgb = props.color === 'white' ? '255, 255, 255' : '0, 0, 0';
return `rgba(${rgb}, ${opacity})`;
}, [props.color, props.opacity]);
useLockScroll(maskRef, visible && preventScrollThrough, overlayClass);

const [active, setActive] = useState(props.visible);
const unmountedRef = useUnmountedRef();
const { opacity } = useSpring({
opacity: props.visible ? 1 : 0,
config: {
precision: 0.01,
mass: 1,
tension: 200,
friction: 30,
clamp: true,
},
onStart: () => {
setActive(true);
},
onRest: () => {
if (unmountedRef.current) return;
setActive(props.visible);
if (props.visible) {
props.afterShow?.();
} else {
props.afterClose?.();
}
},
});
const shouldRender = useShouldRender(active, props.forceRender, props.destroyOnClose);
useEffect(() => {
if (visible) {
setShouldRender(true);
}
}, [visible]);

const node = withStopPropagation(
props.stopPropagation,
withNativeProps(
props,
<animated.div
className={name}
ref={ref}
style={{
background,
opacity,
...props.style,
display: active ? 'unset' : 'none',
}}
onClick={(e) => {
if (e.target === e.currentTarget) {
props.onOverlayClick?.(e);
}
const handleExited = () => {
if (!visible) {
setShouldRender(false);
}
};

const overlayStyles = useMemo(
() => ({
zIndex,
backgroundColor,
...style,
}),
[zIndex, backgroundColor, style],
);

const handleClick = (e) => {
onClick?.({ e });
};

return (
shouldRender && (
<CSSTransition
in={visible}
appear
timeout={duration}
nodeRef={maskRef}
classNames={{
enter: `${overlayClass}-enter-from`,
enterActive: `${overlayClass}-enter-active`,
exit: `${overlayClass}-leave-to`,
exitActive: `${overlayClass}-leave-active`,
}}
onExited={handleExited}
unmountOnExit
>
{props.onOverlayClick && (
<div className={`${classPrefix}-aria-button`} role="button" onClick={props.onOverlayClick} />
)}
<div className={`${classPrefix}-content`}>{shouldRender && props.children}</div>
</animated.div>,
),
<div
ref={maskRef}
className={classNames(className, overlayClass, {
[`${overlayClass}--active`]: visible,
})}
style={overlayStyles}
onClick={handleClick}
>
{parseTNode(children)}
</div>
</CSSTransition>
)
);
});

return renderToContainer(props.getContainer, node);
};

Overlay.defaultProps = defaultProps;
Overlay.displayName = 'Overlay';

export default Overlay;
21 changes: 21 additions & 0 deletions src/overlay/_example/base.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React, { useState } from 'react';
import { Overlay, Button } from 'tdesign-mobile-react';

import './style/index.less';

export default function Base() {
const [visible, setVisible] = useState(false);

const handleVisible1Change = () => {
setVisible(false);
};

return (
<div>
<Button variant="outline" block onClick={() => setVisible(true)}>
基础用法
</Button>
<Overlay visible={visible} duration={0} onClick={handleVisible1Change} />
</div>
);
}
17 changes: 17 additions & 0 deletions src/overlay/_example/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import React from 'react';
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
import BaseDemo from './base';

import './style/index.less';

export default function ProgressDemo() {
return (
<div className="tdesign-mobile-demo">
<TDemoHeader title="Overlay 遮罩层" summary="通过遮罩层,可以强调部分内容" />
<TDemoBlock title="01 组件" summary="基础遮罩层" padding={true}>
<BaseDemo />
</TDemoBlock>
</div>
);
}
3 changes: 3 additions & 0 deletions src/overlay/_example/style/index.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.tdesign-mobile-demo {
background-color: #fff;
}
12 changes: 12 additions & 0 deletions src/overlay/defaultProps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC
* */

import { TdOverlayProps } from './type';

export const overlayDefaultProps: TdOverlayProps = {
duration: 300,
preventScrollThrough: true,
visible: false,
zIndex: 1000,
};
Loading
Loading