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/popover 新增Popover组件 #510

Merged
merged 12 commits into from
Sep 18, 2024
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
Loading