Skip to content

Commit

Permalink
Feat/popover 新增Popover组件 (#510)
Browse files Browse the repository at this point in the history
* feat(popover): 添加popover组件

* feat(popover): 添加popover示例及文档

* feat(popover): 完善出现动画

* feat(popover): 更新测试快照

* feat(popover): 增加displayName属性

* docs(popover): 文档移除vue相关路径

* perf(popover): 调整popper变量声明方式

* docs(popover): 通过api重新生成文档

* chore: update _common

* chore: update snapshot

* perf(popover): 使用useDefault

---------

Co-authored-by: anlyyao <[email protected]>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Sep 18, 2024
1 parent 0a81b26 commit 55cb32d
Show file tree
Hide file tree
Showing 18 changed files with 2,402 additions and 953 deletions.
5 changes: 5 additions & 0 deletions site/mobile/mobile.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ export default {
name: 'overlay',
component: () => import('tdesign-mobile-react/overlay/_example/index.tsx'),
},
{
title: 'Popover 弹出气泡',
name: 'popover',
component: () => import('tdesign-mobile-react/popover/_example/index.tsx'),
},
{
title: 'Popup 弹出层',
name: 'popup',
Expand Down
6 changes: 6 additions & 0 deletions site/web/site.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ export default {
path: '/mobile-react/components/overlay',
component: () => import('tdesign-mobile-react/overlay/overlay.md'),
},
{
title: 'Popover 弹出气泡',
name: 'popover',
path: '/mobile-react/components/popover',
component: () => import('tdesign-mobile-react/popover/popover.md'),
},
{
title: 'Popup 弹出层',
name: 'popup',
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export * from './popup';
export * from './pull-down-refresh';
export * from './toast';
export * from './drawer';
export * from './popover';

/**
* 二期组件
Expand Down
220 changes: 220 additions & 0 deletions src/popover/Popover.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { createPopper, Placement } from '@popperjs/core';
import { useClickAway } from 'ahooks';
import { CSSTransition } from 'react-transition-group';
import { CSSTransitionClassNames } from 'react-transition-group/CSSTransition';
import classNames from 'classnames';
import { TdPopoverProps } from './type';
import { StyledProps } from '../common';
import { usePrefixClass } from '../hooks/useClass';
import useDefaultProps from '../hooks/useDefaultProps';
import { popoverDefaultProps } from './defaultProps';
import { parseContentTNode } from '../_util/parseTNode';
import useDefault from '../_util/useDefault';

export interface PopoverProps extends TdPopoverProps, StyledProps {}

const Popover: React.FC<PopoverProps> = (props) => {
const {
closeOnClickOutside,
className,
style,
content,
placement,
showArrow,
theme,
triggerElement,
children,
visible,
defaultVisible,
onVisibleChange,
} = useDefaultProps<PopoverProps>(props, popoverDefaultProps);

const [currentVisible, setVisible] = useDefault(visible, defaultVisible, onVisibleChange);
const [active, setActive] = useState(visible);
const referenceRef = useRef<HTMLDivElement>(null);
const popoverRef = useRef<HTMLDivElement>(null);
const popperRef = useRef<ReturnType<typeof createPopper> | null>(null);
const popoverClass = usePrefixClass('popover');
const contentClasses = useMemo(
() =>
classNames({
[`${popoverClass}__content`]: true,
[`${popoverClass}--${theme}`]: true,
}),
[popoverClass, theme],
);

const getPopperPlacement = (placement: PopoverProps['placement']): Placement =>
placement?.replace(/-(left|top)$/, '-start').replace(/-(right|bottom)$/, '-end') as Placement;

const placementPadding = ({
popper,
reference,
placement,
}: {
popper: {
width: number;
height: number;
x: number;
y: number;
};
reference: {
width: number;
height: number;
x: number;
y: number;
};
placement: String;
}) => {
const horizontal = ['top', 'bottom'];
const vertical = ['left', 'right'];
const isBase = [...horizontal, ...vertical].find((item) => item === placement);
if (isBase) {
return 0;
}

const { width, x } = reference;
const { width: popperWidth, height: popperHeight } = popper;
const { width: windowWidth } = window.screen;

const isHorizontal = horizontal.find((item) => placement.includes(item));
const isEnd = placement.includes('end');
const small = (a: number, b: number) => (a < b ? a : b);
if (isHorizontal) {
const padding = isEnd ? small(width + x, popperWidth) : small(windowWidth - x, popperWidth);
return {
[isEnd ? 'left' : 'right']: padding - 22,
};
}

const isVertical = vertical.find((item) => placement.includes(item));
if (isVertical) {
return {
[isEnd ? 'top' : 'bottom']: popperHeight - 22,
};
}
};

const getPopoverOptions = () => ({
placement: getPopperPlacement(placement),
modifiers: [
{
name: 'arrow',
options: {
padding: placementPadding,
},
},
],
});

const destroyPopper = () => {
if (popperRef.current) {
popperRef.current?.destroy();
popperRef.current = null;
}
};

const animationClassNames: CSSTransitionClassNames = {
enter: `${popoverClass}--animation-enter`,
enterActive: `${popoverClass}--animation-enter-active ${popoverClass}--animation-enter-to`,
exitActive: `${popoverClass}--animation-leave-active ${popoverClass}--animation-leave-to`,
exitDone: `${popoverClass}--animation-leave-done`,
};

const updatePopper = () => {
if (currentVisible && referenceRef.current && popoverRef.current) {
popperRef.current = createPopper(referenceRef.current, popoverRef.current, getPopoverOptions());
}
return null;
};

const updateVisible = (visible) => {
if (visible === currentVisible) return;
setVisible(visible);
};

const onClickAway = () => {
if (currentVisible && closeOnClickOutside) {
updateVisible(false);
}
};

useClickAway(() => {
onClickAway();
}, [referenceRef.current, popoverRef.current]);

const onClickReference = () => {
updateVisible(!currentVisible);
};

useEffect(() => {
setVisible(visible);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [visible]);

useEffect(() => {
destroyPopper();
updatePopper();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [placement]);

useEffect(
() => () => {
onClickAway();
},
// eslint-disable-next-line react-hooks/exhaustive-deps
[],
);

const contentStyle = useMemo<React.CSSProperties>(
() => ({
display: active ? null : 'none',
}),
[active],
);

const renderArrow = () => <div className={`${popoverClass}__arrow`} data-popper-arrow />;
const renderContentNode = () => (
<div ref={popoverRef} data-popper-placement={placement} className={`${popoverClass}`} style={contentStyle}>
<div className={contentClasses}>
{parseContentTNode(content, {})}
{showArrow && renderArrow()}
</div>
</div>
);

return (
<>
<div
ref={referenceRef}
className={classNames(`${popoverClass}__wrapper`, className)}
style={style}
onClick={onClickReference}
>
{children}
{triggerElement}
</div>
<CSSTransition
in={currentVisible}
classNames={animationClassNames}
timeout={200}
onEnter={() => {
updatePopper();
setActive(true);
}}
onExited={() => {
destroyPopper();
setActive(false);
}}
nodeRef={popoverRef}
>
<>{renderContentNode()}</>
</CSSTransition>
</>
);
};

Popover.displayName = 'Popover';

export default Popover;
25 changes: 25 additions & 0 deletions src/popover/_example/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import React from 'react';
import TDemoBlock from '../../../site/mobile/components/DemoBlock';
import TDemoHeader from '../../../site/mobile/components/DemoHeader';
import TypeDemo from './type';
import ThemeDemo from './theme';
import PlacementDemo from './placement';

import './style/index.less';

export default function LinkDemo() {
return (
<div className="tdesign-mobile-demo">
<TDemoHeader title="Popover 弹出气泡" summary="用于文字提示的气泡框。" />
<TDemoBlock title="01 组件类型">
<TypeDemo />
</TDemoBlock>
<TDemoBlock title="02 组件样式">
<ThemeDemo />
</TDemoBlock>
<TDemoBlock>
<PlacementDemo />
</TDemoBlock>
</div>
);
}
Loading

0 comments on commit 55cb32d

Please sign in to comment.