Skip to content

Commit

Permalink
feat(popup): 对齐vue mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
hkaikai committed Aug 8, 2024
1 parent e0f4055 commit e0e50f4
Show file tree
Hide file tree
Showing 21 changed files with 397 additions and 186 deletions.
2 changes: 1 addition & 1 deletion site/mobile/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import App from './App';
import '../style/mobile/index.less';

import '../../src/_common/style/mobile/_reset.less';
import '../../src/_common/style/mobile/index.less';
// import '../../src/_common/style/mobile/index.less';

ReactDOM.render(
<React.StrictMode>
Expand Down
2 changes: 1 addition & 1 deletion site/web/main.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

import '@common/style/mobile/index.less';
// import '@common/style/mobile/index.less';

import 'tdesign-site-components';
import 'tdesign-site-components/lib/styles/style.css';
Expand Down
22 changes: 22 additions & 0 deletions src/_util/renderTNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { ReactNode } from 'react';
import { TNode } from '../common';

interface JSXRenderContext<T = Record<string, any>> {
defaultNode?: ReactNode;
params?: T;
wrap?: (node: ReactNode) => ReactNode;
}

export const renderTNode = <T = any>(node: TNode<T>, options: JSXRenderContext<T>): ReactNode => {
const wrap = options.wrap ?? ((node) => node);
if (typeof node === 'function') {
return wrap(node(options.params));
}
if (node === true) {
return wrap(options.defaultNode);
}
if (!node) {
return null;
}
return wrap(node);
};
15 changes: 15 additions & 0 deletions src/_util/renderToContainer.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createPortal } from 'react-dom';
import { ReactElement, ReactPortal } from 'react';
import isFunction from 'lodash/isFunction';
import isString from 'lodash/isString';
import { resolveContainer } from './getContainer';
import { canUseDom } from './canUseDom';
import { AttachNode } from '../common';

export type GetContainer = HTMLElement | (() => HTMLElement) | null;

Expand All @@ -12,3 +15,15 @@ export function renderToContainer(getContainer: GetContainer, node: ReactElement
}
return node;
}

export function getAttach(node: AttachNode): HTMLElement {
const attachNode = isFunction(node) ? node() : node;

if (isString(attachNode)) {
return document.querySelector(attachNode);
}
if (attachNode instanceof HTMLElement) {
return attachNode;
}
return document.body;
}
2 changes: 1 addition & 1 deletion src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { ReactElement, ReactNode, CSSProperties, FormEvent, DragEvent, Synthetic
// TElement 表示 API 只接受传入组件
export type TElement<T = undefined> = T extends undefined ? ReactElement : (props: T) => ReactElement;
// 1. TNode = ReactNode; 2. TNode<T> = (props: T) => ReactNode
export type TNode<T = undefined> = T extends undefined ? ReactNode : (props: T) => ReactNode;
export type TNode<T = undefined> = T extends undefined ? ReactNode | (() => ReactNode) : ReactNode | ((props: T) => ReactNode);

export type AttachNodeReturnValue = HTMLElement | Element | Document;
export type AttachNode = CSSSelector | ((triggerNode?: HTMLElement) => AttachNodeReturnValue);
Expand Down
2 changes: 2 additions & 0 deletions src/overlay/Overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface OverlayProps extends NativeProps {
afterClose?: () => void;
stopPropagation?: PropagationEvent[];
children?: React.ReactNode;
duration?: number;
}

const opacityRecord = {
Expand Down Expand Up @@ -62,6 +63,7 @@ const Overlay: FC<OverlayProps> = (props) => {
tension: 200,
friction: 30,
clamp: true,
duration: props.duration,
},
onStart: () => {
setActive(true);
Expand Down
99 changes: 77 additions & 22 deletions src/popup/Popup.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React, { FC, useState } from 'react';
import classnames from 'classnames';
import { useSpring, animated } from 'react-spring';
import { CloseIcon } from 'tdesign-icons-react';
import Overlay from '../overlay';
import useDefault from '../_util/useDefault';
import { PropagationEvent } from '../_util/withStopPropagation';
import withNativeProps, { NativeProps } from '../_util/withNativeProps';
import { TdPopupProps } from './type';
import useConfig from '../_util/useConfig';
import { popupDefaultProps } from './defaultProps';
import { renderToContainer, getAttach } from '../_util/renderToContainer';
import { renderTNode } from '../_util/renderTNode';

export interface PopupProps extends TdPopupProps, NativeProps {}

enum PopupSourceEnum {
OVERLAY = 'overlay',
CLOSEBTN = 'close-btn',
}

enum PlacementEnum {
Expand All @@ -24,28 +27,66 @@ enum PlacementEnum {
}

const Popup: FC<PopupProps> = (props) => {
const { children, placement, showOverlay, visible, defaultVisible, zIndex, overlayProps, onVisibleChange } = props;
const {
children,
placement,
showOverlay,
visible,
defaultVisible,
zIndex,
overlayProps,
preventScrollThrough,
attach,
destroyOnClose,
closeBtn,
closeOnOverlayClick,
onClose,
onClosed,
onOpen,
onOpened,
onVisibleChange,
} = props;

const { classPrefix } = useConfig();

const name = `${classPrefix}-popup`;

const duration = 300;

const [show, setShow] = useDefault<boolean, any>(visible, defaultVisible, onVisibleChange);

const [active, setActive] = useState(show);

const handleOverlayClick = () => {
const handleOverlayClick = (e) => {
if (!closeOnOverlayClick) return;
onClose?.(e);
setShow(false, PopupSourceEnum.OVERLAY);
};

const handleCloseClick = (e) => {
onClose?.(e);
setShow(false, PopupSourceEnum.CLOSEBTN);
};

const { progress, opacity } = useSpring({
progress: show ? 0 : 100,
opacity: show ? 1 : 0,
config: {
duration,
},
onStart: () => {
if (show) {
onOpen?.();
}
setActive(true);
},
onRest: () => {
setActive(show);
if (show) {
onOpened?.();
} else {
onClosed?.();
}
},
});

Expand All @@ -63,36 +104,50 @@ const Popup: FC<PopupProps> = (props) => {
if (placement === PlacementEnum.RIGHT) {
return `translateX(${p}%)`;
}
if (placement === PlacementEnum.CENTER) {
return `scale(${1 - p / 100}) translate3d(-50%, -50%, 0)`;
}
}),
opacity: opacity.to((o) => {
if (placement === PlacementEnum.CENTER) {
return o;
}
}),
};

const rootStyles = {
zIndex,
display: active ? 'block' : 'none',
display: active ? null : 'none',
transition: 'none',
transformOrigin: '0 0',
};

return withNativeProps(
props,
<div className={`${name}`} style={rootStyles}>
{showOverlay && (
<Overlay
visible={show}
onOverlayClick={handleOverlayClick}
disableBodyScroll={false}
stopPropagation={[PropagationEvent.CLICK, PropagationEvent.SCROLL]}
{...overlayProps}
/>
const closeBtnNode = renderTNode(closeBtn, {
defaultNode: <CloseIcon size={24} />,
params: props,
wrap: (node) => (
<div className={`${name}__close`} onClick={handleCloseClick}>
{node}
</div>
),
});
let node = (
<>
<Overlay
visible={show && showOverlay}
onOverlayClick={handleOverlayClick}
disableBodyScroll={preventScrollThrough}
duration={duration}
{...overlayProps}
/>
{withNativeProps(
props,
<animated.div className={classnames([name, `${name}--${placement}`])} style={contentStyle}>
{closeBtnNode}
{children}
</animated.div>,
)}
<animated.div className={classnames([`${name}--content`, `${name}--content-${placement}`])} style={contentStyle}>
{active && children}
</animated.div>
</div>,
</>
);
node = attach ? renderToContainer(getAttach(attach), node) : node;
return (!destroyOnClose || active) && node;
};

Popup.displayName = 'Popup';
Expand Down
77 changes: 21 additions & 56 deletions src/popup/_example/base.jsx
Original file line number Diff line number Diff line change
@@ -1,64 +1,29 @@
import React, { useState } from 'react';
import { Popup, Button } from 'tdesign-mobile-react';
import React from 'react';
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
import './style/index.less'
import './style/index.less';
import PlacementTop from './placement-top';
import PlacementLeft from './placement-left';
import PlacementCenter from './placement-center';
import PlacementBottom from './placement-bottom';
import PlacementRight from './placement-right';
import WithTitle from './with-title';
import CustomClose from './custom-close';

export default function Base() {

const [visible1, setVisible1] = useState(false)
const [visible2, setVisible2] = useState(false)
const [visible3, setVisible3] = useState(false)
const [visible4, setVisible4] = useState(false)
const [visible5, setVisible5] = useState(false)

const handleVisible1Change = (visible) => {
setVisible1(visible);
}
const handleVisible2Change = (visible) => {
setVisible2(visible);
}

const handleVisible3Change = (visible) => {
setVisible3(visible);
}

const handleVisible4Change = (visible) => {
setVisible4(visible);
}

const handleVisible5Change = (visible) => {
setVisible5(visible);
}

return (
<div className='tdesign-mobile-demo'>
<TDemoHeader title="Popup 弹窗层" summary="由其他控件触发,屏幕滑出或弹出一块自定义内容区域"/>
<TDemoBlock title="类型" summary="弹出层出现为止可能为顶部、底部、中部、左侧或右侧">
<div>
<div className='tdesign-mobile-demo__button-group'>
<Button variant="outline" className='tdesign-mobile-demo__button' onClick={() => setVisible1(true)}>顶部弹出</Button>
<Button variant="outline" className='tdesign-mobile-demo__button' onClick={() => setVisible2(true)}>底部弹出</Button>
<Button variant="outline" className='tdesign-mobile-demo__button' onClick={() => setVisible3(true)}>中部弹出</Button>
<Button variant="outline" className='tdesign-mobile-demo__button' onClick={() => setVisible4(true)}>左侧弹出</Button>
<Button variant="outline" className='tdesign-mobile-demo__button' onClick={() => setVisible5(true)}>右侧弹出</Button>
</div>
<Popup visible={visible1} onVisibleChange={handleVisible1Change} placement="top">
<div className="vertical"></div>
</Popup>
<Popup visible={visible2} onVisibleChange={handleVisible2Change} placement="bottom">
<div className="vertical"></div>
</Popup>
<Popup visible={visible3} onVisibleChange={handleVisible3Change} placement="center">
<div className="center"></div>
</Popup>
<Popup visible={visible4} onVisibleChange={handleVisible4Change} placement="left">
<div className="horizontal"></div>
</Popup>
<Popup visible={visible5} onVisibleChange={handleVisible5Change} placement="right">
<div className="horizontal"></div>
</Popup>
</div>
<div className="tdesign-mobile-popup-demo">
<TDemoHeader title="Popup 弹窗层" summary="由其他控件触发,屏幕滑出或弹出一块自定义内容区域" />
<TDemoBlock title="01 组件类型" summary="基础弹出层" padding={true}>
<PlacementTop />
<PlacementLeft />
<PlacementCenter />
<PlacementBottom />
<PlacementRight />
</TDemoBlock>
<TDemoBlock title="01 组件示例" summary="应用示例" padding={true}>
<WithTitle />
<CustomClose />
</TDemoBlock>
</div>
);
Expand Down
35 changes: 35 additions & 0 deletions src/popup/_example/custom-close.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import React, { useState } from 'react';
import { CloseCircleIcon } from 'tdesign-icons-react';
import { Popup, Button } from 'tdesign-mobile-react';

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

const handleVisibleChange = (visible) => {
setVisible(visible);
};

const onHide = () => setVisible(false);

return (
<>
<Button variant="outline" block={true} theme="primary" size="large" onClick={() => setVisible(true)}>
居中弹出层-带自定义关闭按钮
</Button>

<Popup
visible={visible}
onVisibleChange={handleVisibleChange}
placement="center"
style={{ width: '240px', height: '240px' }}
>
<CloseCircleIcon
className="design-mobile-popup-demo__custom-close close-btn"
size={32}
color="#fff"
onClick={onHide}
/>
</Popup>
</>
);
}
Loading

0 comments on commit e0e50f4

Please sign in to comment.