This commit is contained in:
2026-05-25 17:08:18 +08:00
parent df501f6151
commit f1259b71b5
9178 changed files with 1626125 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015-present Alipay.com, https://www.alipay.com/
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+258
View File
@@ -0,0 +1,258 @@
# @rc-component/trigger
React Trigger Component
[![NPM version][npm-image]][npm-url]
[![npm download][download-image]][download-url]
[![build status][github-actions-image]][github-actions-url]
[![Test coverage][codecov-image]][codecov-url]
[![bundle size][bundlephobia-image]][bundlephobia-url]
[![dumi][dumi-image]][dumi-url]
[npm-image]: https://img.shields.io/npm/v/@rc-component/trigger.svg?style=flat-square
[npm-url]: https://npmjs.org/package/@rc-component/trigger
[github-actions-image]: https://github.com/react-component/trigger/actions/workflows/main.yml/badge.svg
[github-actions-url]: https://github.com/react-component/trigger/actions/workflows/main.yml
[codecov-image]: https://img.shields.io/codecov/c/github/react-component/trigger/master.svg?style=flat-square
[codecov-url]: https://codecov.io/gh/react-component/trigger/branch/master
[david-url]: https://david-dm.org/react-component/trigger
[david-image]: https://david-dm.org/react-component/trigger/status.svg?style=flat-square
[david-dev-url]: https://david-dm.org/react-component/trigger?type=dev
[david-dev-image]: https://david-dm.org/react-component/trigger/dev-status.svg?style=flat-square
[download-image]: https://img.shields.io/npm/dm/@rc-component/trigger.svg?style=flat-square
[download-url]: https://npmjs.org/package/@rc-component/trigger
[bundlephobia-url]: https://bundlephobia.com/result?p=@rc-component/trigger
[bundlephobia-image]: https://badgen.net/bundlephobia/minzip/@rc-component/trigger
[dumi-image]: https://img.shields.io/badge/docs%20by-dumi-blue?style=flat-square
[dumi-url]: https://github.com/umijs/dumi
## Install
[![@rc-component/trigger](https://nodei.co/npm/@rc-component/trigger.png)](https://npmjs.org/package/@rc-component/trigger)
## Usage
Include the default [styling](https://github.com/react-component/trigger/blob/master/assets/index.less#L4:L11) and then:
```js
import React from 'react';
import ReactDOM from 'react-dom';
import Trigger from '@rc-component/trigger';
ReactDOM.render(
<Trigger
action={['click']}
popup={<span>popup</span>}
popupAlign={{
points: ['tl', 'bl'],
offset: [0, 3],
}}
>
<a href="#">hover</a>
</Trigger>,
container,
);
```
## Compatibility
| [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/edge/edge_48x48.png" alt="IE / Edge" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>IE / Edge | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png" alt="Firefox" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Firefox | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/chrome/chrome_48x48.png" alt="Chrome" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Chrome | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png" alt="Safari" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Safari | [<img src="https://raw.githubusercontent.com/alrra/browser-logos/master/src/electron/electron_48x48.png" alt="Electron" width="24px" height="24px" />](http://godban.github.io/browsers-support-badges/)<br>Electron |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| IE11, Edge | last 2 versions | last 2 versions | last 2 versions | last 2 versions |
## Example
http://localhost:9001
## Development
```
npm install
npm start
```
## API
### props
<table class="table table-bordered table-striped">
<thead>
<tr>
<th style="width: 100px;">name</th>
<th style="width: 50px;">type</th>
<th style="width: 50px;">default</th>
<th>description</th>
</tr>
</thead>
<tbody>
<tr>
<td>alignPoint</td>
<td>bool</td>
<td>false</td>
<td>Popup will align with mouse position (support action of 'click', 'hover' and 'contextMenu')</td>
</tr>
<tr>
<td>popupClassName</td>
<td>string</td>
<td></td>
<td>additional className added to popup</td>
</tr>
<tr>
<td>forceRender</td>
<td>boolean</td>
<td>false</td>
<td>whether render popup before first show</td>
</tr>
<tr>
<td>destroyPopupOnHide</td>
<td>boolean</td>
<td>false</td>
<td>whether destroy popup when hide</td>
</tr>
<tr>
<td>getPopupClassNameFromAlign</td>
<td>getPopupClassNameFromAlign(align: Object):String</td>
<td></td>
<td>additional className added to popup according to align</td>
</tr>
<tr>
<td>action</td>
<td>string[]</td>
<td>['hover']</td>
<td>which actions cause popup shown. enum of 'hover','click','focus','contextMenu'</td>
</tr>
<tr>
<td>mouseEnterDelay</td>
<td>number</td>
<td>0</td>
<td>delay time to show when mouse enter. unit: s.</td>
</tr>
<tr>
<td>mouseLeaveDelay</td>
<td>number</td>
<td>0.1</td>
<td>delay time to hide when mouse leave. unit: s.</td>
</tr>
<tr>
<td>popupStyle</td>
<td>Object</td>
<td></td>
<td>additional style of popup</td>
</tr>
<tr>
<td>prefixCls</td>
<td>String</td>
<td>rc-trigger-popup</td>
<td>prefix class name</td>
</tr>
<tr>
<td>popupTransitionName</td>
<td>String|Object</td>
<td></td>
<td>https://github.com/react-component/animate</td>
</tr>
<tr>
<td>maskTransitionName</td>
<td>String|Object</td>
<td></td>
<td>https://github.com/react-component/animate</td>
</tr>
<tr>
<td>onPopupVisibleChange</td>
<td>Function</td>
<td></td>
<td>call when popup visible is changed</td>
</tr>
<tr>
<td>mask</td>
<td>boolean</td>
<td>false</td>
<td>whether to support mask</td>
</tr>
<tr>
<td>maskClosable</td>
<td>boolean</td>
<td>true</td>
<td>whether to support click mask to hide</td>
</tr>
<tr>
<td>popupVisible</td>
<td>boolean</td>
<td></td>
<td>whether popup is visible</td>
</tr>
<tr>
<td>zIndex</td>
<td>number</td>
<td></td>
<td>popup's zIndex</td>
</tr>
<tr>
<td>defaultPopupVisible</td>
<td>boolean</td>
<td></td>
<td>whether popup is visible initially</td>
</tr>
<tr>
<td>popupAlign</td>
<td>Object: alignConfig of [dom-align](https://github.com/yiminghe/dom-align)</td>
<td></td>
<td>popup 's align config</td>
</tr>
<tr>
<td>onPopupAlign</td>
<td>function(popupDomNode, align)</td>
<td></td>
<td>callback when popup node is aligned</td>
</tr>
<tr>
<td>popup</td>
<td>React.Element | function() => React.Element</td>
<td></td>
<td>popup content</td>
</tr>
<tr>
<td>getPopupContainer</td>
<td>getPopupContainer(): HTMLElement</td>
<td></td>
<td>function returning html node which will act as popup container</td>
</tr>
<tr>
<td>getDocument</td>
<td>getDocument(): HTMLElement</td>
<td></td>
<td>function returning document node which will be attached click event to close trigger</td>
</tr>
<tr>
<td>popupPlacement</td>
<td>string</td>
<td></td>
<td>use preset popup align config from builtinPlacements, can be merged by popupAlign prop</td>
</tr>
<tr>
<td>builtinPlacements</td>
<td>object</td>
<td></td>
<td>builtin placement align map. used by placement prop</td>
</tr>
<tr>
<td>stretch</td>
<td>string</td>
<td></td>
<td>Let popup div stretch with trigger element. enums of 'width', 'minWidth', 'height', 'minHeight'. (You can also mixed with 'height minWidth')</td>
</tr>
</tbody>
</table>
## Test Case
```
npm test
npm run coverage
```
open coverage/ dir
## License
rc-trigger is released under the MIT license.
+192
View File
@@ -0,0 +1,192 @@
.rc-trigger-popup {
position: absolute;
top: -9999px;
left: -9999px;
z-index: 1050;
}
.rc-trigger-popup-hidden {
display: none;
}
.rc-trigger-popup-zoom-enter,
.rc-trigger-popup-zoom-appear {
opacity: 0;
animation-play-state: paused;
animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28);
animation-duration: 0.3s;
animation-fill-mode: both;
}
.rc-trigger-popup-zoom-leave {
animation-duration: 0.3s;
animation-fill-mode: both;
animation-play-state: paused;
animation-timing-function: cubic-bezier(0.6, -0.3, 0.74, 0.05);
}
.rc-trigger-popup-zoom-enter.rc-trigger-popup-zoom-enter-active,
.rc-trigger-popup-zoom-appear.rc-trigger-popup-zoom-appear-active {
animation-name: rcTriggerZoomIn;
animation-play-state: running;
}
.rc-trigger-popup-zoom-leave.rc-trigger-popup-zoom-leave-active {
animation-name: rcTriggerZoomOut;
animation-play-state: running;
}
.rc-trigger-popup-arrow {
z-index: 1;
width: 0px;
height: 0px;
background: #000;
border-radius: 100vw;
box-shadow: 0 0 0 3px black;
}
@keyframes rcTriggerZoomIn {
0% {
transform: scale(0, 0);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 0;
}
100% {
transform: scale(1, 1);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 1;
}
}
@keyframes rcTriggerZoomOut {
0% {
transform: scale(1, 1);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 1;
}
100% {
transform: scale(0, 0);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 0;
}
}
.rc-trigger-popup-unique-container {
position: absolute;
z-index: 0;
box-sizing: border-box;
border: 1px solid red;
background: green;
}
.rc-trigger-popup-unique-container-hidden {
display: none;
}
.rc-trigger-popup-unique-container-visible {
transition: all 0.1s;
}
.rc-trigger-popup-unique-controlled {
border-color: rgba(0, 0, 0, 0.01) !important;
background: transparent !important;
z-index: 1;
}
.rc-trigger-popup-motion-content-fade-appear {
opacity: 0;
animation-duration: 0.3s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
}
.rc-trigger-popup-motion-content-fade-appear.rc-trigger-popup-motion-content-fade-appear-active {
animation-name: rcTriggerFadeIn;
}
@keyframes rcTriggerFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
.rc-trigger-popup-mask {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: #373737;
background-color: rgba(55, 55, 55, 0.6);
height: 100%;
filter: alpha(opacity=50);
z-index: 1050;
}
.rc-trigger-popup-mask-hidden {
display: none;
}
.rc-trigger-popup-fade-enter,
.rc-trigger-popup-fade-appear {
opacity: 0;
animation-duration: 0.3s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
animation-play-state: paused;
}
.rc-trigger-popup-fade-leave {
animation-duration: 0.3s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
animation-play-state: paused;
}
.rc-trigger-popup-fade-enter.rc-trigger-popup-fade-enter-active,
.rc-trigger-popup-fade-appear.rc-trigger-popup-fade-appear-active {
animation-name: rcTriggerMaskFadeIn;
animation-play-state: running;
}
.rc-trigger-popup-fade-leave.rc-trigger-popup-fade-leave-active {
animation-name: rcDialogFadeOut;
animation-play-state: running;
}
@keyframes rcTriggerMaskFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes rcDialogFadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
.rc-trigger-popup-mobile {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: auto;
}
.rc-trigger-popup-mobile.raise-enter,
.rc-trigger-popup-mobile.raise-appear {
transform: translateY(100%);
}
.rc-trigger-popup-mobile.raise-enter-active,
.rc-trigger-popup-mobile.raise-appear-active {
transition: all 0.3s;
transform: translateY(0);
}
.rc-trigger-popup-mobile.raise-leave {
transform: translateY(0);
}
.rc-trigger-popup-mobile.raise-leave-active {
transition: all 0.3s;
transform: translateY(100%);
}
.rc-trigger-popup-mobile-mask.fade-enter,
.rc-trigger-popup-mobile-mask.fade-appear {
opacity: 0;
}
.rc-trigger-popup-mobile-mask.fade-enter-active,
.rc-trigger-popup-mobile-mask.fade-appear-active {
transition: all 0.3s;
opacity: 1;
}
.rc-trigger-popup-mobile-mask.fade-leave {
opacity: 1;
}
.rc-trigger-popup-mobile-mask.fade-leave-active {
transition: all 0.3s;
opacity: 0;
}
+127
View File
@@ -0,0 +1,127 @@
@triggerPrefixCls: rc-trigger-popup;
.@{triggerPrefixCls} {
position: absolute;
top: -9999px;
left: -9999px;
z-index: 1050;
&-hidden {
display: none;
}
.effect() {
animation-duration: 0.3s;
animation-fill-mode: both;
}
&-zoom-enter,
&-zoom-appear {
opacity: 0;
animation-play-state: paused;
animation-timing-function: cubic-bezier(0.18, 0.89, 0.32, 1.28);
.effect();
}
&-zoom-leave {
.effect();
animation-play-state: paused;
animation-timing-function: cubic-bezier(0.6, -0.3, 0.74, 0.05);
}
&-zoom-enter&-zoom-enter-active,
&-zoom-appear&-zoom-appear-active {
animation-name: rcTriggerZoomIn;
animation-play-state: running;
}
&-zoom-leave&-zoom-leave-active {
animation-name: rcTriggerZoomOut;
animation-play-state: running;
}
&-arrow {
z-index: 1;
width: 0px;
height: 0px;
background: #000;
border-radius: 100vw;
box-shadow: 0 0 0 3px black;
}
@keyframes rcTriggerZoomIn {
0% {
transform: scale(0, 0);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 0;
}
100% {
transform: scale(1, 1);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 1;
}
}
@keyframes rcTriggerZoomOut {
0% {
transform: scale(1, 1);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 1;
}
100% {
transform: scale(0, 0);
transform-origin: var(--arrow-x, 50%) var(--arrow-y, 50%);
opacity: 0;
}
}
// =============== Unique Container ===============
&-unique-container {
position: absolute;
z-index: 0;
box-sizing: border-box;
border: 1px solid red;
background: green;
&-hidden {
display: none;
}
&-visible {
transition: all 0.1s;
}
}
// Debug
&-unique-controlled {
border-color: rgba(0, 0, 0, 0.01) !important;
background: transparent !important;
z-index: 1;
}
// Motion Content
&-motion-content {
// Fade motion
&-fade-appear {
opacity: 0;
animation-duration: 0.3s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
}
&-fade-appear&-fade-appear-active {
animation-name: rcTriggerFadeIn;
}
@keyframes rcTriggerFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
}
}
@import './index/Mask';
@import './index/Mobile';
@@ -0,0 +1,65 @@
.@{triggerPrefixCls} {
&-mask {
position: fixed;
top: 0;
right: 0;
left: 0;
bottom: 0;
background-color: rgb(55, 55, 55);
background-color: rgba(55, 55, 55, 0.6);
height: 100%;
filter: alpha(opacity=50);
z-index: 1050;
&-hidden {
display: none;
}
}
.fade-effect() {
animation-duration: 0.3s;
animation-fill-mode: both;
animation-timing-function: cubic-bezier(0.55, 0, 0.55, 0.2);
}
&-fade-enter,
&-fade-appear {
opacity: 0;
.fade-effect();
animation-play-state: paused;
}
&-fade-leave {
.fade-effect();
animation-play-state: paused;
}
&-fade-enter&-fade-enter-active,
&-fade-appear&-fade-appear-active {
animation-name: rcTriggerMaskFadeIn;
animation-play-state: running;
}
&-fade-leave&-fade-leave-active {
animation-name: rcDialogFadeOut;
animation-play-state: running;
}
@keyframes rcTriggerMaskFadeIn {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
@keyframes rcDialogFadeOut {
0% {
opacity: 1;
}
100% {
opacity: 0;
}
}
}
@@ -0,0 +1,55 @@
.@{triggerPrefixCls} {
&-mobile {
position: fixed;
left: 0;
right: 0;
bottom: 0;
top: auto;
// Motion
&.raise {
&-enter,
&-appear {
transform: translateY(100%);
&-active {
transition: all 0.3s;
transform: translateY(0);
}
}
&-leave {
transform: translateY(0);
&-active {
transition: all 0.3s;
transform: translateY(100%);
}
}
}
// Mask
&-mask {
&.fade {
&-enter,
&-appear {
opacity: 0;
&-active {
transition: all 0.3s;
opacity: 1;
}
}
&-leave {
opacity: 1;
&-active {
transition: all 0.3s;
opacity: 0;
}
}
}
}
}
}
@@ -0,0 +1,9 @@
import * as React from 'react';
import type { AlignType, ArrowPos, ArrowTypeOuter } from '../interface';
export interface ArrowProps {
prefixCls: string;
align: AlignType;
arrow: ArrowTypeOuter;
arrowPos: ArrowPos;
}
export default function Arrow(props: ArrowProps): React.JSX.Element;
+64
View File
@@ -0,0 +1,64 @@
import { clsx } from 'clsx';
import * as React from 'react';
export default function Arrow(props) {
const {
prefixCls,
align,
arrow,
arrowPos
} = props;
const {
className,
content,
style
} = arrow || {};
const {
x = 0,
y = 0
} = arrowPos;
const arrowRef = React.useRef(null);
// Skip if no align
if (!align || !align.points) {
return null;
}
const alignStyle = {
position: 'absolute'
};
// Skip if no need to align
if (align.autoArrow !== false) {
const popupPoints = align.points[0];
const targetPoints = align.points[1];
const popupTB = popupPoints[0];
const popupLR = popupPoints[1];
const targetTB = targetPoints[0];
const targetLR = targetPoints[1];
// Top & Bottom
if (popupTB === targetTB || !['t', 'b'].includes(popupTB)) {
alignStyle.top = y;
} else if (popupTB === 't') {
alignStyle.top = 0;
} else {
alignStyle.bottom = 0;
}
// Left & Right
if (popupLR === targetLR || !['l', 'r'].includes(popupLR)) {
alignStyle.left = x;
} else if (popupLR === 'l') {
alignStyle.left = 0;
} else {
alignStyle.right = 0;
}
}
return /*#__PURE__*/React.createElement("div", {
ref: arrowRef,
className: clsx(`${prefixCls}-arrow`, className),
style: {
...alignStyle,
...style
}
}, content);
}
+11
View File
@@ -0,0 +1,11 @@
import type { CSSMotionProps } from '@rc-component/motion';
import * as React from 'react';
export interface MaskProps {
prefixCls: string;
open?: boolean;
zIndex?: number;
mask?: boolean;
motion?: CSSMotionProps;
mobile?: boolean;
}
export default function Mask(props: MaskProps): React.JSX.Element;
+29
View File
@@ -0,0 +1,29 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import CSSMotion from '@rc-component/motion';
import * as React from 'react';
export default function Mask(props) {
const {
prefixCls,
open,
zIndex,
mask,
motion,
mobile
} = props;
if (!mask) {
return null;
}
return /*#__PURE__*/React.createElement(CSSMotion, _extends({}, motion, {
motionAppear: true,
visible: open,
removeOnLeave: true
}), ({
className
}) => /*#__PURE__*/React.createElement("div", {
style: {
zIndex
},
className: clsx(`${prefixCls}-mask`, mobile && `${prefixCls}-mobile-mask`, className)
}));
}
@@ -0,0 +1,7 @@
import * as React from 'react';
export interface PopupContentProps {
children?: React.ReactNode;
cache?: boolean;
}
declare const PopupContent: React.MemoExoticComponent<({ children }: PopupContentProps) => React.ReactElement<any, string | React.JSXElementConstructor<any>>>;
export default PopupContent;
@@ -0,0 +1,8 @@
import * as React from 'react';
const PopupContent = /*#__PURE__*/React.memo(({
children
}) => children, (_, next) => next.cache);
if (process.env.NODE_ENV !== 'production') {
PopupContent.displayName = 'PopupContent';
}
export default PopupContent;
+57
View File
@@ -0,0 +1,57 @@
import type { CSSMotionProps } from '@rc-component/motion';
import { type ResizeObserverProps } from '@rc-component/resize-observer';
import * as React from 'react';
import type { TriggerProps } from '../';
import type { AlignType, ArrowPos, ArrowTypeOuter } from '../interface';
import type { PortalProps } from '@rc-component/portal';
export interface MobileConfig {
mask?: boolean;
/** Set popup motion. You can ref `rc-motion` for more info. */
motion?: CSSMotionProps;
/** Set mask motion. You can ref `rc-motion` for more info. */
maskMotion?: CSSMotionProps;
}
export interface PopupProps {
onEsc?: PortalProps['onEsc'];
prefixCls: string;
className?: string;
style?: React.CSSProperties;
popup?: TriggerProps['popup'];
target: HTMLElement;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
onPointerEnter?: React.MouseEventHandler<HTMLDivElement>;
onPointerDownCapture?: React.MouseEventHandler<HTMLDivElement>;
zIndex?: number;
mask?: boolean;
onVisibleChanged: (visible: boolean) => void;
align?: AlignType;
arrow?: ArrowTypeOuter;
arrowPos: ArrowPos;
open: boolean;
/** Tell Portal that should keep in screen. e.g. should wait all motion end */
keepDom: boolean;
fresh?: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
motion?: CSSMotionProps;
maskMotion?: CSSMotionProps;
forceRender?: boolean;
getPopupContainer?: TriggerProps['getPopupContainer'];
autoDestroy?: boolean;
portal: React.ComponentType<any>;
children?: React.ReactElement;
ready: boolean;
offsetX: number;
offsetY: number;
offsetR: number;
offsetB: number;
onAlign: VoidFunction;
onPrepare: () => Promise<void>;
stretch?: string;
targetWidth?: number;
targetHeight?: number;
onResize?: ResizeObserverProps['onResize'];
mobile?: MobileConfig;
}
declare const Popup: React.ForwardRefExoticComponent<PopupProps & React.RefAttributes<HTMLDivElement>>;
export default Popup;
+191
View File
@@ -0,0 +1,191 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import { clsx } from 'clsx';
import CSSMotion from '@rc-component/motion';
import ResizeObserver from '@rc-component/resize-observer';
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import { composeRef } from "@rc-component/util/es/ref";
import * as React from 'react';
import Arrow from "./Arrow";
import Mask from "./Mask";
import PopupContent from "./PopupContent";
import useOffsetStyle from "../hooks/useOffsetStyle";
import { useEvent } from '@rc-component/util';
const Popup = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
onEsc,
popup,
className,
prefixCls,
style,
target,
onVisibleChanged,
// Open
open,
keepDom,
fresh,
// Click
onClick,
// Mask
mask,
// Arrow
arrow,
arrowPos,
align,
// Motion
motion,
maskMotion,
// Mobile
mobile,
// Portal
forceRender,
getPopupContainer,
autoDestroy,
portal: Portal,
children,
zIndex,
onMouseEnter,
onMouseLeave,
onPointerEnter,
onPointerDownCapture,
ready,
offsetX,
offsetY,
offsetR,
offsetB,
onAlign,
onPrepare,
// Resize
onResize,
stretch,
targetWidth,
targetHeight
} = props;
const popupContent = typeof popup === 'function' ? popup() : popup;
// We can not remove holder only when motion finished.
const isNodeVisible = open || keepDom;
// ========================= Mobile =========================
const isMobile = !!mobile;
// ========================== Mask ==========================
const [mergedMask, mergedMaskMotion, mergedPopupMotion] = React.useMemo(() => {
if (mobile) {
return [mobile.mask, mobile.maskMotion, mobile.motion];
}
return [mask, maskMotion, motion];
}, [mobile, mask, maskMotion, motion]);
// ======================= Container ========================
const getPopupContainerNeedParams = getPopupContainer?.length > 0;
const [show, setShow] = React.useState(!getPopupContainer || !getPopupContainerNeedParams);
// Delay to show since `getPopupContainer` need target element
useLayoutEffect(() => {
if (!show && getPopupContainerNeedParams && target) {
setShow(true);
}
}, [show, getPopupContainerNeedParams, target]);
// ========================= Resize =========================
const onInternalResize = useEvent((size, ele) => {
onResize?.(size, ele);
onAlign();
});
// ========================= Styles =========================
const offsetStyle = useOffsetStyle(isMobile, ready, open, align, offsetR, offsetB, offsetX, offsetY);
// ========================= Render =========================
if (!show) {
return null;
}
// >>>>> Misc
const miscStyle = {};
if (stretch) {
if (stretch.includes('height') && targetHeight) {
miscStyle.height = targetHeight;
} else if (stretch.includes('minHeight') && targetHeight) {
miscStyle.minHeight = targetHeight;
}
if (stretch.includes('width') && targetWidth) {
miscStyle.width = targetWidth;
} else if (stretch.includes('minWidth') && targetWidth) {
miscStyle.minWidth = targetWidth;
}
}
if (!open) {
miscStyle.pointerEvents = 'none';
}
return /*#__PURE__*/React.createElement(Portal, {
open: forceRender || isNodeVisible,
getContainer: getPopupContainer && (() => getPopupContainer(target)),
autoDestroy: autoDestroy,
onEsc: onEsc
}, /*#__PURE__*/React.createElement(Mask, {
prefixCls: prefixCls,
open: open,
zIndex: zIndex,
mask: mergedMask,
motion: mergedMaskMotion,
mobile: isMobile
}), /*#__PURE__*/React.createElement(ResizeObserver, {
onResize: onInternalResize,
disabled: !open
}, resizeObserverRef => {
return /*#__PURE__*/React.createElement(CSSMotion, _extends({
motionAppear: true,
motionEnter: true,
motionLeave: true,
removeOnLeave: false,
forceRender: forceRender,
leavedClassName: `${prefixCls}-hidden`
}, mergedPopupMotion, {
onAppearPrepare: onPrepare,
onEnterPrepare: onPrepare,
visible: open,
onVisibleChanged: nextVisible => {
motion?.onVisibleChanged?.(nextVisible);
onVisibleChanged(nextVisible);
}
}), ({
className: motionClassName,
style: motionStyle
}, motionRef) => {
const cls = clsx(prefixCls, motionClassName, className, {
[`${prefixCls}-mobile`]: isMobile
});
return /*#__PURE__*/React.createElement("div", {
ref: composeRef(resizeObserverRef, ref, motionRef),
className: cls,
style: {
'--arrow-x': `${arrowPos.x || 0}px`,
'--arrow-y': `${arrowPos.y || 0}px`,
...offsetStyle,
...miscStyle,
...motionStyle,
boxSizing: 'border-box',
zIndex,
...style
},
onMouseEnter: onMouseEnter,
onMouseLeave: onMouseLeave,
onPointerEnter: onPointerEnter,
onClick: onClick,
onPointerDownCapture: onPointerDownCapture
}, arrow && /*#__PURE__*/React.createElement(Arrow, {
prefixCls: prefixCls,
arrow: arrow,
arrowPos: arrowPos,
align: align
}), /*#__PURE__*/React.createElement(PopupContent, {
cache: !open && !fresh
}, popupContent));
});
}), children);
});
if (process.env.NODE_ENV !== 'production') {
Popup.displayName = 'Popup';
}
export default Popup;
@@ -0,0 +1,24 @@
import React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { AlignType, ArrowPos } from '../interface';
export interface UniqueContainerProps {
prefixCls: string;
isMobile: boolean;
ready: boolean;
open: boolean;
align: AlignType;
offsetR: number;
offsetB: number;
offsetX: number;
offsetY: number;
arrowPos?: ArrowPos;
popupSize?: {
width: number;
height: number;
};
motion?: CSSMotionProps;
uniqueContainerClassName?: string;
uniqueContainerStyle?: React.CSSProperties;
}
declare const UniqueContainer: (props: UniqueContainerProps) => React.JSX.Element;
export default UniqueContainer;
@@ -0,0 +1,76 @@
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
import React from 'react';
import useOffsetStyle from "../hooks/useOffsetStyle";
import { clsx } from 'clsx';
import CSSMotion from '@rc-component/motion';
const UniqueContainer = props => {
const {
prefixCls,
isMobile,
ready,
open,
align,
offsetR,
offsetB,
offsetX,
offsetY,
arrowPos,
popupSize,
motion,
uniqueContainerClassName,
uniqueContainerStyle
} = props;
const containerCls = `${prefixCls}-unique-container`;
const [motionVisible, setMotionVisible] = React.useState(false);
// ========================= Styles =========================
const offsetStyle = useOffsetStyle(isMobile, ready, open, align, offsetR, offsetB, offsetX, offsetY);
// Cache for offsetStyle when ready is true
const cachedOffsetStyleRef = React.useRef(offsetStyle);
// Update cached offset style when ready is true
if (ready) {
cachedOffsetStyleRef.current = offsetStyle;
}
// Apply popup size if available
const sizeStyle = {};
if (popupSize) {
sizeStyle.width = popupSize.width;
sizeStyle.height = popupSize.height;
}
// ========================= Render =========================
return /*#__PURE__*/React.createElement(CSSMotion, _extends({
motionAppear: true,
motionEnter: true,
motionLeave: true,
removeOnLeave: false,
leavedClassName: `${containerCls}-hidden`
}, motion, {
visible: open,
onVisibleChanged: nextVisible => {
setMotionVisible(nextVisible);
}
}), ({
className: motionClassName,
style: motionStyle
}) => {
const cls = clsx(containerCls, motionClassName, uniqueContainerClassName, {
[`${containerCls}-visible`]: motionVisible
});
return /*#__PURE__*/React.createElement("div", {
className: cls,
style: {
'--arrow-x': `${arrowPos?.x || 0}px`,
'--arrow-y': `${arrowPos?.y || 0}px`,
...cachedOffsetStyleRef.current,
...sizeStyle,
...motionStyle,
...uniqueContainerStyle
}
});
});
};
export default UniqueContainer;
@@ -0,0 +1,9 @@
import * as React from 'react';
import { type UniqueShowOptions } from '../context';
export interface UniqueProviderProps {
children: React.ReactNode;
/** Additional handle options data to do the customize info */
postTriggerProps?: (options: UniqueShowOptions) => UniqueShowOptions;
}
declare const UniqueProvider: ({ children, postTriggerProps, }: UniqueProviderProps) => React.JSX.Element;
export default UniqueProvider;
@@ -0,0 +1,175 @@
import * as React from 'react';
import Portal from '@rc-component/portal';
import TriggerContext, { UniqueContext } from "../context";
import useDelay from "../hooks/useDelay";
import useAlign from "../hooks/useAlign";
import Popup from "../Popup";
import { useEvent } from '@rc-component/util';
import useTargetState from "./useTargetState";
import { isDOM } from "@rc-component/util/es/Dom/findDOMNode";
import UniqueContainer from "./UniqueContainer";
import { clsx } from 'clsx';
import { getAlignPopupClassName } from "../util";
const UniqueProvider = ({
children,
postTriggerProps
}) => {
const [trigger, open, options, onTargetVisibleChanged] = useTargetState();
// ========================== Options ===========================
const mergedOptions = React.useMemo(() => {
if (!options || !postTriggerProps) {
return options;
}
return postTriggerProps(options);
}, [options, postTriggerProps]);
// =========================== Popup ============================
const [popupEle, setPopupEle] = React.useState(null);
const [popupSize, setPopupSize] = React.useState(null);
// Used for forwardRef popup. Not use internal
const externalPopupRef = React.useRef(null);
const setPopupRef = useEvent(node => {
externalPopupRef.current = node;
if (isDOM(node) && popupEle !== node) {
setPopupEle(node);
}
});
// ========================== Register ==========================
// Store the isOpen function from the latest show call
const isOpenRef = React.useRef(null);
const delayInvoke = useDelay();
const show = useEvent((showOptions, isOpen) => {
// Store the isOpen function for later use in hide
isOpenRef.current = isOpen;
delayInvoke(() => {
trigger(showOptions);
}, showOptions.delay);
});
const hide = delay => {
delayInvoke(() => {
// Check if we should still hide by calling the isOpen function
// If isOpen returns true, it means another trigger wants to keep it open
if (isOpenRef.current?.()) {
return; // Don't hide if something else wants it open
}
trigger(false);
// Don't clear target, currentNode, options immediately, wait until animation completes
}, delay);
};
// Callback after animation completes
const onVisibleChanged = useEvent(visible => {
// Call useTargetState callback to handle animation state
onTargetVisibleChanged(visible);
});
// =========================== Align ============================
const [ready, offsetX, offsetY, offsetR, offsetB, arrowX, arrowY,
// scaleX - not used in UniqueProvider
,,
// scaleY - not used in UniqueProvider
alignInfo, onAlign] = useAlign(open, popupEle, mergedOptions?.target, mergedOptions?.popupPlacement, mergedOptions?.builtinPlacements || {}, mergedOptions?.popupAlign, undefined,
// onPopupAlign
false // isMobile
);
const alignedClassName = React.useMemo(() => {
if (!mergedOptions) {
return '';
}
const baseClassName = getAlignPopupClassName(mergedOptions.builtinPlacements || {}, mergedOptions.prefixCls || '', alignInfo, false // alignPoint is false for UniqueProvider
);
return clsx(baseClassName, mergedOptions.getPopupClassNameFromAlign?.(alignInfo));
}, [alignInfo, mergedOptions?.getPopupClassNameFromAlign, mergedOptions?.builtinPlacements, mergedOptions?.prefixCls]);
const contextValue = React.useMemo(() => ({
show,
hide
}), []);
// =========================== Align ============================
React.useEffect(() => {
onAlign();
}, [mergedOptions?.target]);
// =========================== Motion ===========================
const onPrepare = useEvent(() => {
onAlign();
return Promise.resolve();
});
// ======================== Trigger Context =====================
const subPopupElements = React.useRef({});
const parentContext = React.useContext(TriggerContext);
const triggerContextValue = React.useMemo(() => ({
registerSubPopup: (id, subPopupEle) => {
subPopupElements.current[id] = subPopupEle;
parentContext?.registerSubPopup(id, subPopupEle);
}
}), [parentContext]);
// =========================== Render ===========================
const prefixCls = mergedOptions?.prefixCls;
return /*#__PURE__*/React.createElement(UniqueContext.Provider, {
value: contextValue
}, children, mergedOptions && /*#__PURE__*/React.createElement(TriggerContext.Provider, {
value: triggerContextValue
}, /*#__PURE__*/React.createElement(Popup, {
ref: setPopupRef,
portal: Portal,
onEsc: mergedOptions.onEsc,
prefixCls: prefixCls,
popup: mergedOptions.popup,
className: clsx(mergedOptions.popupClassName, alignedClassName, `${prefixCls}-unique-controlled`),
style: mergedOptions.popupStyle,
target: mergedOptions.target,
open: open,
keepDom: true,
fresh: true,
autoDestroy: false,
onVisibleChanged: onVisibleChanged,
ready: ready,
offsetX: offsetX,
offsetY: offsetY,
offsetR: offsetR,
offsetB: offsetB,
onAlign: onAlign,
onPrepare: onPrepare,
onResize: size => setPopupSize({
width: size.offsetWidth,
height: size.offsetHeight
}),
arrowPos: {
x: arrowX,
y: arrowY
},
align: alignInfo,
zIndex: mergedOptions.zIndex,
mask: mergedOptions.mask,
arrow: mergedOptions.arrow,
motion: mergedOptions.popupMotion,
maskMotion: mergedOptions.maskMotion,
getPopupContainer: mergedOptions.getPopupContainer
}, /*#__PURE__*/React.createElement(UniqueContainer, {
prefixCls: prefixCls,
isMobile: false,
ready: ready,
open: open,
align: alignInfo,
offsetR: offsetR,
offsetB: offsetB,
offsetX: offsetX,
offsetY: offsetY,
arrowPos: {
x: arrowX,
y: arrowY
},
popupSize: popupSize,
motion: mergedOptions.popupMotion,
uniqueContainerClassName: clsx(mergedOptions.uniqueContainerClassName, alignedClassName),
uniqueContainerStyle: mergedOptions.uniqueContainerStyle
}))));
};
export default UniqueProvider;
@@ -0,0 +1,16 @@
import type { UniqueShowOptions } from '../context';
/**
* Control the state of popup bind target:
* 1. When set `target`. Do show the popup.
* 2. When `target` is removed. Do hide the popup.
* 3. When `target` change to another one:
* a. We wait motion finish of previous popup.
* b. Then we set new target and show the popup.
* 4. During appear/enter animation, cache new options and apply after animation completes.
*/
export default function useTargetState(): [
trigger: (options: UniqueShowOptions | false) => void,
open: boolean,
cacheOptions: UniqueShowOptions | null,
onVisibleChanged: (visible: boolean) => void
];
@@ -0,0 +1,55 @@
import React from 'react';
import { useEvent } from '@rc-component/util';
/**
* Control the state of popup bind target:
* 1. When set `target`. Do show the popup.
* 2. When `target` is removed. Do hide the popup.
* 3. When `target` change to another one:
* a. We wait motion finish of previous popup.
* b. Then we set new target and show the popup.
* 4. During appear/enter animation, cache new options and apply after animation completes.
*/
export default function useTargetState() {
const [options, setOptions] = React.useState(null);
const [open, setOpen] = React.useState(false);
const [isAnimating, setIsAnimating] = React.useState(false);
const pendingOptionsRef = React.useRef(null);
const trigger = useEvent(nextOptions => {
if (nextOptions === false) {
// Clear pending options when hiding
pendingOptionsRef.current = null;
setOpen(false);
} else {
if (isAnimating && open) {
// If animating (appear or enter), cache new options
pendingOptionsRef.current = nextOptions;
} else {
setOpen(true);
// Set new options
setOptions(nextOptions);
pendingOptionsRef.current = null;
// Only mark as animating when transitioning from closed to open
if (!open) {
setIsAnimating(true);
}
}
}
});
const onVisibleChanged = useEvent(visible => {
if (visible) {
// Animation enter completed, check if there are pending options
setIsAnimating(false);
if (pendingOptionsRef.current) {
// Apply pending options
setOptions(pendingOptionsRef.current);
pendingOptionsRef.current = null;
}
} else {
// Animation leave completed
setIsAnimating(false);
pendingOptionsRef.current = null;
}
});
return [trigger, open, options, onVisibleChanged];
}
+38
View File
@@ -0,0 +1,38 @@
import * as React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { PortalProps } from '@rc-component/portal';
import type { TriggerProps } from './index';
import type { AlignType, ArrowTypeOuter, BuildInPlacements } from './interface';
export interface TriggerContextProps {
registerSubPopup: (id: string, node: HTMLElement) => void;
}
declare const TriggerContext: React.Context<TriggerContextProps>;
export default TriggerContext;
export interface UniqueShowOptions {
id: string;
popup: TriggerProps['popup'];
target: HTMLElement;
delay: number;
prefixCls?: string;
popupClassName?: string;
uniqueContainerClassName?: string;
uniqueContainerStyle?: React.CSSProperties;
popupStyle?: React.CSSProperties;
popupPlacement?: string;
builtinPlacements?: BuildInPlacements;
popupAlign?: AlignType;
zIndex?: number;
mask?: boolean;
maskClosable?: boolean;
popupMotion?: CSSMotionProps;
maskMotion?: CSSMotionProps;
arrow?: ArrowTypeOuter;
getPopupContainer?: TriggerProps['getPopupContainer'];
getPopupClassNameFromAlign?: (align: AlignType) => string;
onEsc?: PortalProps['onEsc'];
}
export interface UniqueContextProps {
show: (options: UniqueShowOptions, isOpen: () => boolean) => void;
hide: (delay: number) => void;
}
export declare const UniqueContext: React.Context<UniqueContextProps>;
+10
View File
@@ -0,0 +1,10 @@
import * as React from 'react';
// ===================== Nest =====================
const TriggerContext = /*#__PURE__*/React.createContext(null);
export default TriggerContext;
// ==================== Unique ====================
export const UniqueContext = /*#__PURE__*/React.createContext(null);
@@ -0,0 +1,5 @@
import type { ActionType } from '../interface';
type InternalActionType = ActionType | 'touch';
type ActionTypes = InternalActionType | InternalActionType[];
export default function useAction(action: ActionTypes, showAction?: ActionTypes, hideAction?: ActionTypes): [showAction: Set<InternalActionType>, hideAction: Set<InternalActionType>];
export {};
@@ -0,0 +1,19 @@
import * as React from 'react';
function toArray(val) {
return val ? Array.isArray(val) ? val : [val] : [];
}
export default function useAction(action, showAction, hideAction) {
return React.useMemo(() => {
const mergedShowAction = toArray(showAction ?? action);
const mergedHideAction = toArray(hideAction ?? action);
const showActionSet = new Set(mergedShowAction);
const hideActionSet = new Set(mergedHideAction);
if (showActionSet.has('hover') && !showActionSet.has('click')) {
showActionSet.add('touch');
}
if (hideActionSet.has('hover') && !hideActionSet.has('click')) {
hideActionSet.add('touch');
}
return [showActionSet, hideActionSet];
}, [action, showAction, hideAction]);
}
@@ -0,0 +1,15 @@
import type { TriggerProps } from '..';
import type { AlignType } from '../interface';
export default function useAlign(open: boolean, popupEle: HTMLElement, target: HTMLElement | [x: number, y: number], placement: string, builtinPlacements: any, popupAlign?: AlignType, onPopupAlign?: TriggerProps['onPopupAlign'], mobile?: boolean): [
ready: boolean,
offsetX: number,
offsetY: number,
offsetR: number,
offsetB: number,
arrowX: number,
arrowY: number,
scaleX: number,
scaleY: number,
align: AlignType,
onAlign: VoidFunction
];
+531
View File
@@ -0,0 +1,531 @@
import { isDOM } from "@rc-component/util/es/Dom/findDOMNode";
import isVisible from "@rc-component/util/es/Dom/isVisible";
import useEvent from "@rc-component/util/es/hooks/useEvent";
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import * as React from 'react';
import { collectScroller, getVisibleArea, getWin, toNum } from "../util";
function getUnitOffset(size, offset = 0) {
const offsetStr = `${offset}`;
const cells = offsetStr.match(/^(.*)\%$/);
if (cells) {
return size * (parseFloat(cells[1]) / 100);
}
return parseFloat(offsetStr);
}
function getNumberOffset(rect, offset) {
const [offsetX, offsetY] = offset || [];
return [getUnitOffset(rect.width, offsetX), getUnitOffset(rect.height, offsetY)];
}
function splitPoints(points = '') {
return [points[0], points[1]];
}
function getAlignPoint(rect, points) {
const topBottom = points[0];
const leftRight = points[1];
let x;
let y;
// Top & Bottom
if (topBottom === 't') {
y = rect.y;
} else if (topBottom === 'b') {
y = rect.y + rect.height;
} else {
y = rect.y + rect.height / 2;
}
// Left & Right
if (leftRight === 'l') {
x = rect.x;
} else if (leftRight === 'r') {
x = rect.x + rect.width;
} else {
x = rect.x + rect.width / 2;
}
return {
x,
y
};
}
function reversePoints(points, index) {
const reverseMap = {
t: 'b',
b: 't',
l: 'r',
r: 'l'
};
const clone = [...points];
clone[index] = reverseMap[points[index]] || 'c';
return clone;
}
function flatPoints(points) {
return points.join('');
}
export default function useAlign(open, popupEle, target, placement, builtinPlacements, popupAlign, onPopupAlign, mobile) {
const [offsetInfo, setOffsetInfo] = React.useState({
ready: false,
offsetX: 0,
offsetY: 0,
offsetR: 0,
offsetB: 0,
arrowX: 0,
arrowY: 0,
scaleX: 1,
scaleY: 1,
align: builtinPlacements[placement] || {}
});
const alignCountRef = React.useRef(0);
const scrollerList = React.useMemo(() => {
if (!popupEle || mobile) {
return [];
}
return collectScroller(popupEle);
}, [popupEle]);
// ========================= Flip ==========================
// We will memo flip info.
// If size change to make flip, it will memo the flip info and use it in next align.
const prevFlipRef = React.useRef({});
const resetFlipCache = () => {
prevFlipRef.current = {};
};
if (!open) {
resetFlipCache();
}
// ========================= Align =========================
const onAlign = useEvent(() => {
if (popupEle && target && open && !mobile) {
const popupElement = popupEle;
const doc = popupElement.ownerDocument;
const win = getWin(popupElement);
const {
position: popupPosition
} = win.getComputedStyle(popupElement);
const originLeft = popupElement.style.left;
const originTop = popupElement.style.top;
const originRight = popupElement.style.right;
const originBottom = popupElement.style.bottom;
const originOverflow = popupElement.style.overflow;
// Placement
const placementInfo = {
...builtinPlacements[placement],
...popupAlign
};
// placeholder element
const placeholderElement = doc.createElement('div');
popupElement.parentElement?.appendChild(placeholderElement);
placeholderElement.style.left = `${popupElement.offsetLeft}px`;
placeholderElement.style.top = `${popupElement.offsetTop}px`;
placeholderElement.style.position = popupPosition;
placeholderElement.style.height = `${popupElement.offsetHeight}px`;
placeholderElement.style.width = `${popupElement.offsetWidth}px`;
// Reset first
popupElement.style.left = '0';
popupElement.style.top = '0';
popupElement.style.right = 'auto';
popupElement.style.bottom = 'auto';
popupElement.style.overflow = 'hidden';
// Calculate align style, we should consider `transform` case
let targetRect;
if (Array.isArray(target)) {
targetRect = {
x: target[0],
y: target[1],
width: 0,
height: 0
};
} else {
const rect = target.getBoundingClientRect();
rect.x = rect.x ?? rect.left;
rect.y = rect.y ?? rect.top;
targetRect = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
};
}
const popupRect = popupElement.getBoundingClientRect();
const {
height,
width
} = win.getComputedStyle(popupElement);
popupRect.x = popupRect.x ?? popupRect.left;
popupRect.y = popupRect.y ?? popupRect.top;
const {
clientWidth,
clientHeight,
scrollWidth,
scrollHeight,
scrollTop,
scrollLeft
} = doc.documentElement;
const popupHeight = popupRect.height;
const popupWidth = popupRect.width;
const targetHeight = targetRect.height;
const targetWidth = targetRect.width;
// Get bounding of visible area
const visibleRegion = {
left: 0,
top: 0,
right: clientWidth,
bottom: clientHeight
};
const scrollRegion = {
left: -scrollLeft,
top: -scrollTop,
right: scrollWidth - scrollLeft,
bottom: scrollHeight - scrollTop
};
let {
htmlRegion
} = placementInfo;
const VISIBLE = 'visible';
const VISIBLE_FIRST = 'visibleFirst';
if (htmlRegion !== 'scroll' && htmlRegion !== VISIBLE_FIRST) {
htmlRegion = VISIBLE;
}
const isVisibleFirst = htmlRegion === VISIBLE_FIRST;
const scrollRegionArea = getVisibleArea(scrollRegion, scrollerList);
const visibleRegionArea = getVisibleArea(visibleRegion, scrollerList);
const visibleArea = htmlRegion === VISIBLE ? visibleRegionArea : scrollRegionArea;
// When set to `visibleFirst`,
// the check `adjust` logic will use `visibleRegion` for check first.
const adjustCheckVisibleArea = isVisibleFirst ? visibleRegionArea : visibleArea;
// Record right & bottom align data
popupElement.style.left = 'auto';
popupElement.style.top = 'auto';
popupElement.style.right = '0';
popupElement.style.bottom = '0';
const popupMirrorRect = popupElement.getBoundingClientRect();
// Reset back
popupElement.style.left = originLeft;
popupElement.style.top = originTop;
popupElement.style.right = originRight;
popupElement.style.bottom = originBottom;
popupElement.style.overflow = originOverflow;
popupElement.parentElement?.removeChild(placeholderElement);
// Calculate scale
const scaleX = toNum(Math.round(popupWidth / parseFloat(width) * 1000) / 1000);
const scaleY = toNum(Math.round(popupHeight / parseFloat(height) * 1000) / 1000);
// No need to align since it's not visible in view
if (scaleX === 0 || scaleY === 0 || isDOM(target) && !isVisible(target)) {
return;
}
// Offset
const {
offset,
targetOffset
} = placementInfo;
let [popupOffsetX, popupOffsetY] = getNumberOffset(popupRect, offset);
const [targetOffsetX, targetOffsetY] = getNumberOffset(targetRect, targetOffset);
targetRect.x -= targetOffsetX;
targetRect.y -= targetOffsetY;
// Points
const [popupPoint, targetPoint] = placementInfo.points || [];
const targetPoints = splitPoints(targetPoint);
const popupPoints = splitPoints(popupPoint);
const targetAlignPoint = getAlignPoint(targetRect, targetPoints);
const popupAlignPoint = getAlignPoint(popupRect, popupPoints);
// Real align info may not same as origin one
const nextAlignInfo = {
...placementInfo
};
let nextPoints = [popupPoints, targetPoints];
// Next Offset
let nextOffsetX = targetAlignPoint.x - popupAlignPoint.x + popupOffsetX;
let nextOffsetY = targetAlignPoint.y - popupAlignPoint.y + popupOffsetY;
// ============== Intersection ===============
// Get area by position. Used for check if flip area is better
function getIntersectionVisibleArea(offsetX, offsetY, area = visibleArea) {
const l = popupRect.x + offsetX;
const t = popupRect.y + offsetY;
const r = l + popupWidth;
const b = t + popupHeight;
const visibleL = Math.max(l, area.left);
const visibleT = Math.max(t, area.top);
const visibleR = Math.min(r, area.right);
const visibleB = Math.min(b, area.bottom);
return Math.max(0, (visibleR - visibleL) * (visibleB - visibleT));
}
const originIntersectionVisibleArea = getIntersectionVisibleArea(nextOffsetX, nextOffsetY);
// As `visibleFirst`, we prepare this for check
const originIntersectionRecommendArea = getIntersectionVisibleArea(nextOffsetX, nextOffsetY, visibleRegionArea);
// ========================== Overflow ===========================
const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']);
const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']);
const targetAlignPointBR = getAlignPoint(targetRect, ['b', 'r']);
const popupAlignPointBR = getAlignPoint(popupRect, ['b', 'r']);
const overflow = placementInfo.overflow || {};
const {
adjustX,
adjustY,
shiftX,
shiftY
} = overflow;
const supportAdjust = val => {
if (typeof val === 'boolean') {
return val;
}
return val >= 0;
};
// Prepare position
let nextPopupY;
let nextPopupBottom;
let nextPopupX;
let nextPopupRight;
function syncNextPopupPosition() {
nextPopupY = popupRect.y + nextOffsetY;
nextPopupBottom = nextPopupY + popupHeight;
nextPopupX = popupRect.x + nextOffsetX;
nextPopupRight = nextPopupX + popupWidth;
}
syncNextPopupPosition();
// >>>>>>>>>> Top & Bottom
const needAdjustY = supportAdjust(adjustY);
const sameTB = popupPoints[0] === targetPoints[0];
// Bottom to Top
if (needAdjustY && popupPoints[0] === 't' && (nextPopupBottom > adjustCheckVisibleArea.bottom || prevFlipRef.current.bt)) {
let tmpNextOffsetY = nextOffsetY;
if (sameTB) {
tmpNextOffsetY -= popupHeight - targetHeight;
} else {
tmpNextOffsetY = targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY;
}
const newVisibleArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.bt = true;
nextOffsetY = tmpNextOffsetY;
popupOffsetY = -popupOffsetY;
nextPoints = [reversePoints(nextPoints[0], 0), reversePoints(nextPoints[1], 0)];
} else {
prevFlipRef.current.bt = false;
}
}
// Top to Bottom
if (needAdjustY && popupPoints[0] === 'b' && (nextPopupY < adjustCheckVisibleArea.top || prevFlipRef.current.tb)) {
let tmpNextOffsetY = nextOffsetY;
if (sameTB) {
tmpNextOffsetY += popupHeight - targetHeight;
} else {
tmpNextOffsetY = targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY;
}
const newVisibleArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.tb = true;
nextOffsetY = tmpNextOffsetY;
popupOffsetY = -popupOffsetY;
nextPoints = [reversePoints(nextPoints[0], 0), reversePoints(nextPoints[1], 0)];
} else {
prevFlipRef.current.tb = false;
}
}
// >>>>>>>>>> Left & Right
const needAdjustX = supportAdjust(adjustX);
// >>>>> Flip
const sameLR = popupPoints[1] === targetPoints[1];
// Right to Left
if (needAdjustX && popupPoints[1] === 'l' && (nextPopupRight > adjustCheckVisibleArea.right || prevFlipRef.current.rl)) {
let tmpNextOffsetX = nextOffsetX;
if (sameLR) {
tmpNextOffsetX -= popupWidth - targetWidth;
} else {
tmpNextOffsetX = targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX;
}
const newVisibleArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.rl = true;
nextOffsetX = tmpNextOffsetX;
popupOffsetX = -popupOffsetX;
nextPoints = [reversePoints(nextPoints[0], 1), reversePoints(nextPoints[1], 1)];
} else {
prevFlipRef.current.rl = false;
}
}
// Left to Right
if (needAdjustX && popupPoints[1] === 'r' && (nextPopupX < adjustCheckVisibleArea.left || prevFlipRef.current.lr)) {
let tmpNextOffsetX = nextOffsetX;
if (sameLR) {
tmpNextOffsetX += popupWidth - targetWidth;
} else {
tmpNextOffsetX = targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX;
}
const newVisibleArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.lr = true;
nextOffsetX = tmpNextOffsetX;
popupOffsetX = -popupOffsetX;
nextPoints = [reversePoints(nextPoints[0], 1), reversePoints(nextPoints[1], 1)];
} else {
prevFlipRef.current.lr = false;
}
}
nextAlignInfo.points = [flatPoints(nextPoints[0]), flatPoints(nextPoints[1])];
// ============================ Shift ============================
syncNextPopupPosition();
const numShiftX = shiftX === true ? 0 : shiftX;
if (typeof numShiftX === 'number') {
// Left
if (nextPopupX < visibleRegionArea.left) {
nextOffsetX -= nextPopupX - visibleRegionArea.left - popupOffsetX;
if (targetRect.x + targetWidth < visibleRegionArea.left + numShiftX) {
nextOffsetX += targetRect.x - visibleRegionArea.left + targetWidth - numShiftX;
}
}
// Right
if (nextPopupRight > visibleRegionArea.right) {
nextOffsetX -= nextPopupRight - visibleRegionArea.right - popupOffsetX;
if (targetRect.x > visibleRegionArea.right - numShiftX) {
nextOffsetX += targetRect.x - visibleRegionArea.right + numShiftX;
}
}
}
const numShiftY = shiftY === true ? 0 : shiftY;
if (typeof numShiftY === 'number') {
// Top
if (nextPopupY < visibleRegionArea.top) {
nextOffsetY -= nextPopupY - visibleRegionArea.top - popupOffsetY;
// When target if far away from visible area
// Stop shift
if (targetRect.y + targetHeight < visibleRegionArea.top + numShiftY) {
nextOffsetY += targetRect.y - visibleRegionArea.top + targetHeight - numShiftY;
}
}
// Bottom
if (nextPopupBottom > visibleRegionArea.bottom) {
nextOffsetY -= nextPopupBottom - visibleRegionArea.bottom - popupOffsetY;
if (targetRect.y > visibleRegionArea.bottom - numShiftY) {
nextOffsetY += targetRect.y - visibleRegionArea.bottom + numShiftY;
}
}
}
// ============================ Arrow ============================
// Arrow center align
const popupLeft = popupRect.x + nextOffsetX;
const popupRight = popupLeft + popupWidth;
const popupTop = popupRect.y + nextOffsetY;
const popupBottom = popupTop + popupHeight;
const targetLeft = targetRect.x;
const targetRight = targetLeft + targetWidth;
const targetTop = targetRect.y;
const targetBottom = targetTop + targetHeight;
/** Max left of the popup and target element */
const maxLeft = Math.max(popupLeft, targetLeft);
/** Min right of the popup and target element */
const minRight = Math.min(popupRight, targetRight);
/** The center X of popup & target cross area */
const xCenter = (maxLeft + minRight) / 2;
/** Arrow X of popup offset */
const nextArrowX = xCenter - popupLeft;
const maxTop = Math.max(popupTop, targetTop);
const minBottom = Math.min(popupBottom, targetBottom);
const yCenter = (maxTop + minBottom) / 2;
const nextArrowY = yCenter - popupTop;
onPopupAlign?.(popupEle, nextAlignInfo);
// Additional calculate right & bottom position
let offsetX4Right = popupMirrorRect.right - popupRect.x - (nextOffsetX + popupRect.width);
let offsetY4Bottom = popupMirrorRect.bottom - popupRect.y - (nextOffsetY + popupRect.height);
if (scaleX === 1) {
nextOffsetX = Math.floor(nextOffsetX);
offsetX4Right = Math.floor(offsetX4Right);
}
if (scaleY === 1) {
nextOffsetY = Math.floor(nextOffsetY);
offsetY4Bottom = Math.floor(offsetY4Bottom);
}
const nextOffsetInfo = {
ready: true,
offsetX: nextOffsetX / scaleX,
offsetY: nextOffsetY / scaleY,
offsetR: offsetX4Right / scaleX,
offsetB: offsetY4Bottom / scaleY,
arrowX: nextArrowX / scaleX,
arrowY: nextArrowY / scaleY,
scaleX,
scaleY,
align: nextAlignInfo
};
setOffsetInfo(nextOffsetInfo);
}
});
const triggerAlign = () => {
alignCountRef.current += 1;
const id = alignCountRef.current;
// Merge all align requirement into one frame
Promise.resolve().then(() => {
if (alignCountRef.current === id) {
onAlign();
}
});
};
// Reset ready status when placement & open changed
const resetReady = () => {
setOffsetInfo(ori => ({
...ori,
ready: false
}));
};
useLayoutEffect(resetReady, [placement]);
useLayoutEffect(() => {
if (!open) {
resetReady();
}
}, [open]);
return [offsetInfo.ready, offsetInfo.offsetX, offsetInfo.offsetY, offsetInfo.offsetR, offsetInfo.offsetB, offsetInfo.arrowX, offsetInfo.arrowY, offsetInfo.scaleX, offsetInfo.scaleY, offsetInfo.align, triggerAlign];
}
@@ -0,0 +1 @@
export default function useDelay(): (callback: VoidFunction, delay: number) => void;
@@ -0,0 +1,28 @@
import * as React from 'react';
export default function useDelay() {
const delayRef = React.useRef(null);
const clearDelay = () => {
if (delayRef.current) {
clearTimeout(delayRef.current);
delayRef.current = null;
}
};
const delayInvoke = (callback, delay) => {
clearDelay();
if (delay === 0) {
callback();
} else {
delayRef.current = setTimeout(() => {
callback();
}, delay * 1000);
}
};
// Clean up on unmount
React.useEffect(() => {
return () => {
clearDelay();
};
}, []);
return delayInvoke;
}
@@ -0,0 +1,3 @@
import type * as React from 'react';
import type { AlignType } from '../interface';
export default function useOffsetStyle(isMobile: boolean, ready: boolean, open: boolean, align: AlignType, offsetR: number, offsetB: number, offsetX: number, offsetY: number): React.CSSProperties;
@@ -0,0 +1,35 @@
export default function useOffsetStyle(isMobile, ready, open, align, offsetR, offsetB, offsetX, offsetY) {
// >>>>> Offset
const AUTO = 'auto';
const offsetStyle = isMobile ? {} : {
left: '-1000vw',
top: '-1000vh',
right: AUTO,
bottom: AUTO
};
// Set align style
if (!isMobile && (ready || !open)) {
const {
points
} = align;
const dynamicInset = align.dynamicInset || align._experimental?.dynamicInset;
const alignRight = dynamicInset && points[0][1] === 'r';
const alignBottom = dynamicInset && points[0][0] === 'b';
if (alignRight) {
offsetStyle.right = offsetR;
offsetStyle.left = AUTO;
} else {
offsetStyle.left = offsetX;
offsetStyle.right = AUTO;
}
if (alignBottom) {
offsetStyle.bottom = offsetB;
offsetStyle.top = AUTO;
} else {
offsetStyle.top = offsetY;
offsetStyle.bottom = AUTO;
}
}
return offsetStyle;
}
@@ -0,0 +1 @@
export default function useWatch(open: boolean, target: HTMLElement, popup: HTMLElement, onAlign: VoidFunction, onScroll: VoidFunction): void;
@@ -0,0 +1,35 @@
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import { collectScroller, getWin } from "../util";
export default function useWatch(open, target, popup, onAlign, onScroll) {
useLayoutEffect(() => {
if (open && target && popup) {
const targetElement = target;
const popupElement = popup;
const targetScrollList = collectScroller(targetElement);
const popupScrollList = collectScroller(popupElement);
const win = getWin(popupElement);
const mergedList = new Set([win, ...targetScrollList, ...popupScrollList]);
function notifyScroll() {
onAlign();
onScroll();
}
mergedList.forEach(scroller => {
scroller.addEventListener('scroll', notifyScroll, {
passive: true
});
});
win.addEventListener('resize', notifyScroll, {
passive: true
});
// First time always do align
onAlign();
return () => {
mergedList.forEach(scroller => {
scroller.removeEventListener('scroll', notifyScroll);
win.removeEventListener('resize', notifyScroll);
});
};
}
}, [open, target, popup]);
}
@@ -0,0 +1,5 @@
/**
* Close if click on the window.
* Return the function that click on the Popup element.
*/
export default function useWinClick(open: boolean, clickToHide: boolean, targetEle: HTMLElement, popupEle: HTMLElement, mask: boolean, maskClosable: boolean, inPopupOrChild: (target: EventTarget) => boolean, triggerOpen: (open: boolean) => void): () => void;
@@ -0,0 +1,59 @@
import { getShadowRoot } from "@rc-component/util/es/Dom/shadow";
import { warning } from "@rc-component/util/es/warning";
import * as React from 'react';
import { getWin } from "../util";
/**
* Close if click on the window.
* Return the function that click on the Popup element.
*/
export default function useWinClick(open, clickToHide, targetEle, popupEle, mask, maskClosable, inPopupOrChild, triggerOpen) {
const openRef = React.useRef(open);
openRef.current = open;
const popupPointerDownRef = React.useRef(false);
// Click to hide is special action since click popup element should not hide
React.useEffect(() => {
if (clickToHide && popupEle && (!mask || maskClosable)) {
const onPointerDown = () => {
popupPointerDownRef.current = false;
};
const onTriggerClose = e => {
if (openRef.current && !inPopupOrChild(e.composedPath?.()?.[0] || e.target) && !popupPointerDownRef.current) {
triggerOpen(false);
}
};
const win = getWin(popupEle);
win.addEventListener('pointerdown', onPointerDown, true);
win.addEventListener('mousedown', onTriggerClose, true);
win.addEventListener('contextmenu', onTriggerClose, true);
// shadow root
const targetShadowRoot = getShadowRoot(targetEle);
if (targetShadowRoot) {
targetShadowRoot.addEventListener('mousedown', onTriggerClose, true);
targetShadowRoot.addEventListener('contextmenu', onTriggerClose, true);
}
// Warning if target and popup not in same root
if (process.env.NODE_ENV !== 'production' && targetEle) {
const targetRoot = targetEle.getRootNode?.();
const popupRoot = popupEle.getRootNode?.();
warning(targetRoot === popupRoot, `trigger element and popup element should in same shadow root.`);
}
return () => {
win.removeEventListener('pointerdown', onPointerDown, true);
win.removeEventListener('mousedown', onTriggerClose, true);
win.removeEventListener('contextmenu', onTriggerClose, true);
if (targetShadowRoot) {
targetShadowRoot.removeEventListener('mousedown', onTriggerClose, true);
targetShadowRoot.removeEventListener('contextmenu', onTriggerClose, true);
}
};
}
}, [clickToHide, targetEle, popupEle, mask, maskClosable]);
function onPopupPointerDown() {
popupPointerDownRef.current = true;
}
return onPopupPointerDown;
}
+80
View File
@@ -0,0 +1,80 @@
import type { CSSMotionProps } from '@rc-component/motion';
import * as React from 'react';
import { type MobileConfig } from './Popup';
import type { ActionType, AlignType, ArrowTypeOuter, BuildInPlacements } from './interface';
export type { ActionType, AlignType, ArrowTypeOuter as ArrowType, BuildInPlacements, };
import UniqueProvider, { type UniqueProviderProps } from './UniqueProvider';
export { UniqueProvider };
export type { UniqueProviderProps };
export interface TriggerRef {
nativeElement: HTMLElement;
popupElement: HTMLDivElement;
forceAlign: VoidFunction;
}
export interface TriggerProps {
children: React.ReactElement<any> | ((info: {
open: boolean;
}) => React.ReactElement<any>);
action?: ActionType | ActionType[];
showAction?: ActionType[];
hideAction?: ActionType[];
prefixCls?: string;
zIndex?: number;
onPopupAlign?: (element: HTMLElement, align: AlignType) => void;
stretch?: string;
popupVisible?: boolean;
defaultPopupVisible?: boolean;
onOpenChange?: (visible: boolean) => void;
afterOpenChange?: (visible: boolean) => void;
/** @deprecated Use `onOpenChange` instead */
onPopupVisibleChange?: (visible: boolean) => void;
/** @deprecated Use `afterOpenChange` instead */
afterPopupVisibleChange?: (visible: boolean) => void;
getPopupContainer?: (node: HTMLElement) => HTMLElement;
forceRender?: boolean;
autoDestroy?: boolean;
mask?: boolean;
maskClosable?: boolean;
/** Set popup motion. You can ref `rc-motion` for more info. */
popupMotion?: CSSMotionProps;
/** Set mask motion. You can ref `rc-motion` for more info. */
maskMotion?: CSSMotionProps;
mouseEnterDelay?: number;
mouseLeaveDelay?: number;
focusDelay?: number;
blurDelay?: number;
popup: React.ReactNode | (() => React.ReactNode);
popupPlacement?: string;
builtinPlacements?: BuildInPlacements;
popupAlign?: AlignType;
popupClassName?: string;
/** Pass to `UniqueProvider` UniqueContainer */
uniqueContainerClassName?: string;
/** Pass to `UniqueProvider` UniqueContainer */
uniqueContainerStyle?: React.CSSProperties;
popupStyle?: React.CSSProperties;
getPopupClassNameFromAlign?: (align: AlignType) => string;
onPopupClick?: React.MouseEventHandler<HTMLDivElement>;
alignPoint?: boolean;
/**
* Trigger will memo content when close.
* This may affect the case if want to keep content update.
* Set `fresh` to `false` will always keep update.
*/
fresh?: boolean;
/**
* Config with UniqueProvider to shared the floating popup.
*/
unique?: boolean;
arrow?: boolean | ArrowTypeOuter;
/**
* @private Bump fixed position at bottom in mobile.
* Will replace the config of root props.
* This will directly trade as mobile view which will not check what real is.
* This is internal usage currently, do not use in your prod.
*/
mobile?: MobileConfig;
}
export declare function generateTrigger(PortalComponent?: React.ComponentType<any>): React.ForwardRefExoticComponent<TriggerProps & React.RefAttributes<TriggerRef>>;
declare const _default: React.ForwardRefExoticComponent<TriggerProps & React.RefAttributes<TriggerRef>>;
export default _default;
+562
View File
@@ -0,0 +1,562 @@
import Portal from '@rc-component/portal';
import { clsx } from 'clsx';
import { useResizeObserver } from '@rc-component/resize-observer';
import { getDOM, isDOM } from "@rc-component/util/es/Dom/findDOMNode";
import { getShadowRoot } from "@rc-component/util/es/Dom/shadow";
import { getNodeRef, useComposeRef } from "@rc-component/util/es/ref";
import useEvent from "@rc-component/util/es/hooks/useEvent";
import useId from "@rc-component/util/es/hooks/useId";
import useLayoutEffect from "@rc-component/util/es/hooks/useLayoutEffect";
import * as React from 'react';
import Popup from "./Popup";
import TriggerContext, { UniqueContext } from "./context";
import useAction from "./hooks/useAction";
import useAlign from "./hooks/useAlign";
import useDelay from "./hooks/useDelay";
import useWatch from "./hooks/useWatch";
import useWinClick from "./hooks/useWinClick";
import { getAlignPopupClassName } from "./util";
import UniqueProvider from "./UniqueProvider";
import { useControlledState } from '@rc-component/util';
import { flushSync } from 'react-dom';
export { UniqueProvider };
// Removed Props List
// Seems this can be auto
// getDocument?: (element?: HTMLElement) => Document;
// New version will not wrap popup with `rc-trigger-popup-content` when multiple children
export function generateTrigger(PortalComponent = Portal) {
const Trigger = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls = 'rc-trigger-popup',
children,
// Action
action = 'hover',
showAction,
hideAction,
// Open
popupVisible,
defaultPopupVisible,
onOpenChange,
afterOpenChange,
onPopupVisibleChange,
afterPopupVisibleChange,
// Delay
mouseEnterDelay,
mouseLeaveDelay = 0.1,
focusDelay,
blurDelay,
// Mask
mask,
maskClosable = true,
// Portal
getPopupContainer,
forceRender,
autoDestroy,
// Popup
popup,
popupClassName,
uniqueContainerClassName,
uniqueContainerStyle,
popupStyle,
popupPlacement,
builtinPlacements = {},
popupAlign,
zIndex,
stretch,
getPopupClassNameFromAlign,
fresh,
unique,
alignPoint,
onPopupClick,
onPopupAlign,
// Arrow
arrow,
// Motion
popupMotion,
maskMotion,
// Private
mobile,
...restProps
} = props;
const mergedAutoDestroy = autoDestroy || false;
const openUncontrolled = popupVisible === undefined;
// =========================== Mobile ===========================
const isMobile = !!mobile;
// ========================== Context ===========================
const subPopupElements = React.useRef({});
const parentContext = React.useContext(TriggerContext);
const context = React.useMemo(() => {
return {
registerSubPopup: (id, subPopupEle) => {
subPopupElements.current[id] = subPopupEle;
parentContext?.registerSubPopup(id, subPopupEle);
}
};
}, [parentContext]);
// ======================== UniqueContext =========================
const uniqueContext = React.useContext(UniqueContext);
// =========================== Popup ============================
const id = useId();
const [popupEle, setPopupEle] = React.useState(null);
// Used for forwardRef popup. Not use internal
const externalPopupRef = React.useRef(null);
const setPopupRef = useEvent(node => {
externalPopupRef.current = node;
if (isDOM(node) && popupEle !== node) {
setPopupEle(node);
}
parentContext?.registerSubPopup(id, node);
});
// =========================== Target ===========================
// Use state to control here since `useRef` update not trigger render
const [targetEle, setTargetEle] = React.useState(null);
// Used for forwardRef target. Not use internal
const externalForwardRef = React.useRef(null);
const setTargetRef = useEvent(node => {
const domNode = getDOM(node);
if (isDOM(domNode) && targetEle !== domNode) {
setTargetEle(domNode);
externalForwardRef.current = domNode;
}
});
const cloneProps = {};
const inPopupOrChild = useEvent(ele => {
const childDOM = targetEle;
return childDOM?.contains(ele) || getShadowRoot(childDOM)?.host === ele || ele === childDOM || popupEle?.contains(ele) || getShadowRoot(popupEle)?.host === ele || ele === popupEle || Object.values(subPopupElements.current).some(subPopupEle => subPopupEle?.contains(ele) || ele === subPopupEle);
});
// =========================== Arrow ============================
const innerArrow = arrow ? {
// true and Object likely
...(arrow !== true ? arrow : {})
} : null;
// ============================ Open ============================
const [internalOpen, setInternalOpen] = useControlledState(defaultPopupVisible || false, popupVisible);
const mergedOpen = internalOpen || false;
// ========================== Children ==========================
const child = React.useMemo(() => {
const nextChild = typeof children === 'function' ? children({
open: mergedOpen
}) : children;
return React.Children.only(nextChild);
}, [children, mergedOpen]);
const originChildProps = child?.props || {};
// Support ref
const isOpen = useEvent(() => mergedOpen);
// Extract common options for UniqueProvider
const getUniqueOptions = useEvent((delay = 0) => ({
popup,
target: targetEle,
delay,
prefixCls,
popupClassName,
uniqueContainerClassName,
uniqueContainerStyle,
popupStyle,
popupPlacement,
builtinPlacements,
popupAlign,
zIndex,
mask,
maskClosable,
popupMotion,
maskMotion,
arrow: innerArrow,
getPopupContainer,
getPopupClassNameFromAlign,
id,
onEsc
}));
// Handle controlled state changes for UniqueProvider
// Only sync to UniqueProvider when it's controlled mode
// If there is a parentContext, don't call uniqueContext methods
useLayoutEffect(() => {
if (uniqueContext && unique && targetEle && !openUncontrolled && !parentContext) {
if (mergedOpen) {
uniqueContext.show(getUniqueOptions(mouseEnterDelay), isOpen);
} else {
uniqueContext.hide(mouseLeaveDelay);
}
}
}, [mergedOpen, targetEle]);
const openRef = React.useRef(mergedOpen);
openRef.current = mergedOpen;
const internalTriggerOpen = useEvent(nextOpen => {
flushSync(() => {
if (mergedOpen !== nextOpen) {
setInternalOpen(nextOpen);
onOpenChange?.(nextOpen);
onPopupVisibleChange?.(nextOpen);
}
});
});
// Trigger for delay
const delayInvoke = useDelay();
const triggerOpen = (nextOpen, delay = 0) => {
// If it's controlled mode, always use internal trigger logic
// UniqueProvider will be synced through useLayoutEffect
if (popupVisible !== undefined) {
delayInvoke(() => {
internalTriggerOpen(nextOpen);
}, delay);
return;
}
// If UniqueContext exists and not controlled, pass delay to Provider instead of handling it internally
// If there is a parentContext, don't call uniqueContext methods
if (uniqueContext && unique && openUncontrolled && !parentContext) {
if (nextOpen) {
uniqueContext.show(getUniqueOptions(delay), isOpen);
} else {
uniqueContext.hide(delay);
}
return;
}
delayInvoke(() => {
internalTriggerOpen(nextOpen);
}, delay);
};
function onEsc({
top
}) {
if (top) {
triggerOpen(false);
}
}
// ========================== Motion ============================
const [inMotion, setInMotion] = React.useState(false);
useLayoutEffect(firstMount => {
if (!firstMount || mergedOpen) {
setInMotion(true);
}
}, [mergedOpen]);
const [motionPrepareResolve, setMotionPrepareResolve] = React.useState(null);
// =========================== Align ============================
const [mousePos, setMousePos] = React.useState(null);
const setMousePosByEvent = event => {
setMousePos([event.clientX, event.clientY]);
};
const [ready, offsetX, offsetY, offsetR, offsetB, arrowX, arrowY, scaleX, scaleY, alignInfo, onAlign] = useAlign(mergedOpen, popupEle, alignPoint && mousePos !== null ? mousePos : targetEle, popupPlacement, builtinPlacements, popupAlign, onPopupAlign, isMobile);
const [showActions, hideActions] = useAction(action, showAction, hideAction);
const clickToShow = showActions.has('click');
const clickToHide = hideActions.has('click') || hideActions.has('contextMenu');
const triggerAlign = useEvent(() => {
if (!inMotion) {
onAlign();
}
});
const onScroll = () => {
if (openRef.current && alignPoint && clickToHide) {
triggerOpen(false);
}
};
useWatch(mergedOpen, targetEle, popupEle, triggerAlign, onScroll);
useLayoutEffect(() => {
triggerAlign();
}, [mousePos, popupPlacement]);
// When no builtinPlacements and popupAlign changed
useLayoutEffect(() => {
if (mergedOpen && !builtinPlacements?.[popupPlacement]) {
triggerAlign();
}
}, [JSON.stringify(popupAlign)]);
const alignedClassName = React.useMemo(() => {
const baseClassName = getAlignPopupClassName(builtinPlacements, prefixCls, alignInfo, alignPoint);
return clsx(baseClassName, getPopupClassNameFromAlign?.(alignInfo));
}, [alignInfo, getPopupClassNameFromAlign, builtinPlacements, prefixCls, alignPoint]);
// ============================ Refs ============================
React.useImperativeHandle(ref, () => ({
nativeElement: externalForwardRef.current,
popupElement: externalPopupRef.current,
forceAlign: triggerAlign
}));
// ========================== Stretch ===========================
const [targetWidth, setTargetWidth] = React.useState(0);
const [targetHeight, setTargetHeight] = React.useState(0);
const syncTargetSize = () => {
if (stretch && targetEle) {
const rect = targetEle.getBoundingClientRect();
setTargetWidth(rect.width);
setTargetHeight(rect.height);
}
};
const onTargetResize = () => {
syncTargetSize();
triggerAlign();
};
// ========================== Motion ============================
const onVisibleChanged = visible => {
setInMotion(false);
onAlign();
afterOpenChange?.(visible);
afterPopupVisibleChange?.(visible);
};
// We will trigger align when motion is in prepare
const onPrepare = () => new Promise(resolve => {
syncTargetSize();
setMotionPrepareResolve(() => resolve);
});
useLayoutEffect(() => {
if (motionPrepareResolve) {
onAlign();
motionPrepareResolve();
setMotionPrepareResolve(null);
}
}, [motionPrepareResolve]);
// =========================== Action ===========================
/**
* Util wrapper for trigger action
* @param eventName Listen event name
* @param nextOpen Next open state after trigger
* @param delay Delay to trigger open change
* @param callback Callback if current event need additional action
* @param ignoreCheck Ignore current event if check return true
*/
function wrapperAction(eventName, nextOpen, delay, callback, ignoreCheck) {
cloneProps[eventName] = (event, ...args) => {
if (!ignoreCheck || !ignoreCheck()) {
callback?.(event);
triggerOpen(nextOpen, delay);
}
// Pass to origin
originChildProps[eventName]?.(event, ...args);
};
}
// ======================= Action: Touch ========================
const touchToShow = showActions.has('touch');
const touchToHide = hideActions.has('touch');
/** Used for prevent `hover` event conflict with mobile env */
const touchedRef = React.useRef(false);
if (touchToShow || touchToHide) {
cloneProps.onTouchStart = (...args) => {
touchedRef.current = true;
if (openRef.current && touchToHide) {
triggerOpen(false);
} else if (!openRef.current && touchToShow) {
triggerOpen(true);
}
// Pass to origin
originChildProps.onTouchStart?.(...args);
};
}
// ======================= Action: Click ========================
if (clickToShow || clickToHide) {
cloneProps.onClick = (event, ...args) => {
if (openRef.current && clickToHide) {
triggerOpen(false);
} else if (!openRef.current && clickToShow) {
setMousePosByEvent(event);
triggerOpen(true);
}
// Pass to origin
originChildProps.onClick?.(event, ...args);
touchedRef.current = false;
};
}
// Click to hide is special action since click popup element should not hide
const onPopupPointerDown = useWinClick(mergedOpen, clickToHide || touchToHide, targetEle, popupEle, mask, maskClosable, inPopupOrChild, triggerOpen);
// ======================= Action: Hover ========================
const hoverToShow = showActions.has('hover');
const hoverToHide = hideActions.has('hover');
let onPopupMouseEnter;
let onPopupMouseLeave;
const ignoreMouseTrigger = () => {
return touchedRef.current;
};
if (hoverToShow) {
const onMouseEnterCallback = event => {
setMousePosByEvent(event);
};
// Compatible with old browser which not support pointer event
wrapperAction('onMouseEnter', true, mouseEnterDelay, onMouseEnterCallback, ignoreMouseTrigger);
wrapperAction('onPointerEnter', true, mouseEnterDelay, onMouseEnterCallback, ignoreMouseTrigger);
onPopupMouseEnter = event => {
// Only trigger re-open when popup is visible
if ((mergedOpen || inMotion) && popupEle?.contains(event.target)) {
triggerOpen(true, mouseEnterDelay);
}
};
// Align Point
if (alignPoint) {
cloneProps.onMouseMove = event => {
originChildProps.onMouseMove?.(event);
};
}
}
if (hoverToHide) {
wrapperAction('onMouseLeave', false, mouseLeaveDelay, undefined, ignoreMouseTrigger);
wrapperAction('onPointerLeave', false, mouseLeaveDelay, undefined, ignoreMouseTrigger);
onPopupMouseLeave = () => {
triggerOpen(false, mouseLeaveDelay);
};
}
// ======================= Action: Focus ========================
if (showActions.has('focus')) {
wrapperAction('onFocus', true, focusDelay);
}
if (hideActions.has('focus')) {
wrapperAction('onBlur', false, blurDelay);
}
// ==================== Action: ContextMenu =====================
if (showActions.has('contextMenu')) {
cloneProps.onContextMenu = (event, ...args) => {
if (openRef.current && hideActions.has('contextMenu')) {
triggerOpen(false);
} else {
setMousePosByEvent(event);
triggerOpen(true);
}
event.preventDefault();
// Pass to origin
originChildProps.onContextMenu?.(event, ...args);
};
}
// ============================ Perf ============================
const rendedRef = React.useRef(false);
rendedRef.current ||= forceRender || mergedOpen || inMotion;
// =========================== Render ===========================
const mergedChildrenProps = {
...originChildProps,
...cloneProps
};
// Pass props into cloneProps for nest usage
const passedProps = {};
const passedEventList = ['onContextMenu', 'onClick', 'onMouseDown', 'onTouchStart', 'onMouseEnter', 'onMouseLeave', 'onFocus', 'onBlur'];
passedEventList.forEach(eventName => {
if (restProps[eventName]) {
passedProps[eventName] = (...args) => {
mergedChildrenProps[eventName]?.(...args);
restProps[eventName](...args);
};
}
});
const arrowPos = {
x: arrowX,
y: arrowY
};
// =================== Resize Observer ===================
// Use hook to observe target element resize
// Pass targetEle directly instead of a function so the hook will re-observe when target changes
useResizeObserver(mergedOpen, targetEle, onTargetResize);
// Compose refs
const mergedRef = useComposeRef(setTargetRef, getNodeRef(child));
// Child Node
const triggerNode = /*#__PURE__*/React.cloneElement(child, {
...mergedChildrenProps,
...passedProps,
ref: mergedRef
});
// Render
return /*#__PURE__*/React.createElement(React.Fragment, null, triggerNode, rendedRef.current && (!uniqueContext || !unique) && /*#__PURE__*/React.createElement(TriggerContext.Provider, {
value: context
}, /*#__PURE__*/React.createElement(Popup, {
portal: PortalComponent,
ref: setPopupRef,
prefixCls: prefixCls,
popup: popup,
className: clsx(popupClassName, !isMobile && alignedClassName),
style: popupStyle,
target: targetEle,
onMouseEnter: onPopupMouseEnter,
onMouseLeave: onPopupMouseLeave
// https://github.com/ant-design/ant-design/issues/43924
,
onPointerEnter: onPopupMouseEnter,
zIndex: zIndex
// Open
,
open: mergedOpen,
keepDom: inMotion,
fresh: fresh
// Click
,
onClick: onPopupClick,
onPointerDownCapture: onPopupPointerDown
// Mask
,
mask: mask
// Motion
,
motion: popupMotion,
maskMotion: maskMotion,
onVisibleChanged: onVisibleChanged,
onPrepare: onPrepare
// Portal
,
forceRender: forceRender,
autoDestroy: mergedAutoDestroy,
getPopupContainer: getPopupContainer,
onEsc: onEsc
// Arrow
,
align: alignInfo,
arrow: innerArrow,
arrowPos: arrowPos
// Align
,
ready: ready,
offsetX: offsetX,
offsetY: offsetY,
offsetR: offsetR,
offsetB: offsetB,
onAlign: triggerAlign
// Stretch
,
stretch: stretch,
targetWidth: targetWidth / scaleX,
targetHeight: targetHeight / scaleY
// Mobile
,
mobile: mobile
})));
});
if (process.env.NODE_ENV !== 'production') {
Trigger.displayName = 'Trigger';
}
return Trigger;
}
export default generateTrigger(Portal);
+92
View File
@@ -0,0 +1,92 @@
/// <reference types="react" />
export type Placement = 'top' | 'left' | 'right' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom';
export type AlignPointTopBottom = 't' | 'b' | 'c';
export type AlignPointLeftRight = 'l' | 'r' | 'c';
/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
export type AlignPoint = `${AlignPointTopBottom}${AlignPointLeftRight}`;
export type OffsetType = number | `${number}%`;
export interface AlignType {
/**
* move point of source node to align with point of target node.
* Such as ['tr','cc'], align top right point of source node with center point of target node.
* Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */
points?: (string | AlignPoint)[];
/**
* @private Do not use in your production code
*/
_experimental?: Record<string, any>;
/**
* offset source node by offset[0] in x and offset[1] in y.
* If offset contains percentage string value, it is relative to sourceNode region.
*/
offset?: OffsetType[];
/**
* offset target node by offset[0] in x and offset[1] in y.
* If targetOffset contains percentage string value, it is relative to targetNode region.
*/
targetOffset?: OffsetType[];
/**
* If adjustX field is true, will adjust source node in x direction if source node is invisible.
* If adjustY field is true, will adjust source node in y direction if source node is invisible.
*/
overflow?: {
adjustX?: boolean | number;
adjustY?: boolean | number;
shiftX?: boolean | number;
shiftY?: boolean | number;
};
/** Auto adjust arrow position */
autoArrow?: boolean;
/**
* Config visible region check of html node. Default `visible`:
* - `visible`:
* The visible region of user browser window.
* Use `clientHeight` for check.
* If `visible` region not satisfy, fallback to `scroll`.
* - `scroll`:
* The whole region of the html scroll area.
* Use `scrollHeight` for check.
* - `visibleFirst`:
* Similar to `visible`, but if `visible` region not satisfy, fallback to `scroll`.
*/
htmlRegion?: 'visible' | 'scroll' | 'visibleFirst';
/**
* Auto chose position with `top` or `bottom` by the align result
*/
dynamicInset?: boolean;
/**
* Whether use css right instead of left to position
*/
useCssRight?: boolean;
/**
* Whether use css bottom instead of top to position
*/
useCssBottom?: boolean;
/**
* Whether use css transform instead of left/top/right/bottom to position if browser supports.
* Defaults to false.
*/
useCssTransform?: boolean;
ignoreShake?: boolean;
}
export interface ArrowTypeOuter {
style?: React.CSSProperties;
className?: string;
content?: React.ReactNode;
}
export type ArrowPos = {
x?: number;
y?: number;
};
export type BuildInPlacements = Record<string, AlignType>;
export type StretchType = string;
export type ActionType = 'hover' | 'focus' | 'click' | 'contextMenu';
export type AnimationType = string;
export type TransitionNameType = string;
export interface Point {
pageX: number;
pageY: number;
}
export interface CommonEventHandler {
remove: () => void;
}
+1
View File
@@ -0,0 +1 @@
export {};
+5
View File
@@ -0,0 +1,5 @@
import * as React from 'react';
import { UniqueProvider } from './index';
declare const _default: React.ForwardRefExoticComponent<import("./index").TriggerProps & React.RefAttributes<import("./index").TriggerRef>>;
export default _default;
export { UniqueProvider };
+23
View File
@@ -0,0 +1,23 @@
import * as React from 'react';
import { generateTrigger, UniqueProvider } from "./index";
const MockPortal = ({
open,
autoDestroy,
children,
getContainer
}) => {
const [visible, setVisible] = React.useState(open);
React.useEffect(() => {
getContainer?.();
});
React.useEffect(() => {
if (open) {
setVisible(true);
} else if (autoDestroy) {
setVisible(false);
}
}, [open, autoDestroy]);
return visible ? children : null;
};
export default generateTrigger(MockPortal);
export { UniqueProvider };
+44
View File
@@ -0,0 +1,44 @@
import type { AlignType, BuildInPlacements } from './interface';
export declare function getAlignPopupClassName(builtinPlacements: BuildInPlacements, prefixCls: string, align: AlignType, isAlignPoint: boolean): string;
export declare function getWin(ele: HTMLElement): Window & typeof globalThis;
/**
* Get all the scrollable parent elements of the element
* @param ele The element to be detected
* @param areaOnly Only return the parent which will cut visible area
*/
export declare function collectScroller(ele: HTMLElement): HTMLElement[];
export declare function toNum(num: number, defaultValue?: number): number;
export interface VisibleArea {
left: number;
top: number;
right: number;
bottom: number;
}
/**
*
*
* **************************************
* * Border *
* * ************************** *
* * * * * *
* * B * * S * B *
* * o * * c * o *
* * r * Content * r * r *
* * d * * o * d *
* * e * * l * e *
* * r ******************** l * r *
* * * Scroll * *
* * ************************** *
* * Border *
* **************************************
*
*/
/**
* Get visible area of element
*/
export declare function getVisibleArea(initArea: VisibleArea, scrollerList?: HTMLElement[]): {
left: number;
top: number;
right: number;
bottom: number;
};
+137
View File
@@ -0,0 +1,137 @@
function isPointsEq(a1 = [], a2 = [], isAlignPoint) {
const getVal = (a, index) => a[index] || '';
if (isAlignPoint) {
return getVal(a1, 0) === getVal(a2, 0);
}
return getVal(a1, 0) === getVal(a2, 0) && getVal(a1, 1) === getVal(a2, 1);
}
export function getAlignPopupClassName(builtinPlacements, prefixCls, align, isAlignPoint) {
const {
points
} = align;
const placements = Object.keys(builtinPlacements);
for (let i = 0; i < placements.length; i += 1) {
const placement = placements[i];
if (isPointsEq(builtinPlacements[placement]?.points, points, isAlignPoint)) {
return `${prefixCls}-placement-${placement}`;
}
}
return '';
}
export function getWin(ele) {
return ele.ownerDocument.defaultView;
}
/**
* Get all the scrollable parent elements of the element
* @param ele The element to be detected
* @param areaOnly Only return the parent which will cut visible area
*/
export function collectScroller(ele) {
const scrollerList = [];
let current = ele?.parentElement;
const scrollStyle = ['hidden', 'scroll', 'clip', 'auto'];
while (current) {
const {
overflowX,
overflowY,
overflow
} = getWin(current).getComputedStyle(current);
if ([overflowX, overflowY, overflow].some(o => scrollStyle.includes(o))) {
scrollerList.push(current);
}
current = current.parentElement;
}
return scrollerList;
}
export function toNum(num, defaultValue = 1) {
return Number.isNaN(num) ? defaultValue : num;
}
function getPxValue(val) {
return toNum(parseFloat(val), 0);
}
/**
*
*
* **************************************
* * Border *
* * ************************** *
* * * * * *
* * B * * S * B *
* * o * * c * o *
* * r * Content * r * r *
* * d * * o * d *
* * e * * l * e *
* * r ******************** l * r *
* * * Scroll * *
* * ************************** *
* * Border *
* **************************************
*
*/
/**
* Get visible area of element
*/
export function getVisibleArea(initArea, scrollerList) {
const visibleArea = {
...initArea
};
(scrollerList || []).forEach(ele => {
if (ele instanceof HTMLBodyElement || ele instanceof HTMLHtmlElement) {
return;
}
// Skip if static position which will not affect visible area
const {
overflow,
overflowClipMargin,
borderTopWidth,
borderBottomWidth,
borderLeftWidth,
borderRightWidth
} = getWin(ele).getComputedStyle(ele);
const eleRect = ele.getBoundingClientRect();
const {
offsetHeight: eleOutHeight,
clientHeight: eleInnerHeight,
offsetWidth: eleOutWidth,
clientWidth: eleInnerWidth
} = ele;
const borderTopNum = getPxValue(borderTopWidth);
const borderBottomNum = getPxValue(borderBottomWidth);
const borderLeftNum = getPxValue(borderLeftWidth);
const borderRightNum = getPxValue(borderRightWidth);
const scaleX = toNum(Math.round(eleRect.width / eleOutWidth * 1000) / 1000);
const scaleY = toNum(Math.round(eleRect.height / eleOutHeight * 1000) / 1000);
// Original visible area
const eleScrollWidth = (eleOutWidth - eleInnerWidth - borderLeftNum - borderRightNum) * scaleX;
const eleScrollHeight = (eleOutHeight - eleInnerHeight - borderTopNum - borderBottomNum) * scaleY;
// Cut border size
const scaledBorderTopWidth = borderTopNum * scaleY;
const scaledBorderBottomWidth = borderBottomNum * scaleY;
const scaledBorderLeftWidth = borderLeftNum * scaleX;
const scaledBorderRightWidth = borderRightNum * scaleX;
// Clip margin
let clipMarginWidth = 0;
let clipMarginHeight = 0;
if (overflow === 'clip') {
const clipNum = getPxValue(overflowClipMargin);
clipMarginWidth = clipNum * scaleX;
clipMarginHeight = clipNum * scaleY;
}
// Region
const eleLeft = eleRect.x + scaledBorderLeftWidth - clipMarginWidth;
const eleTop = eleRect.y + scaledBorderTopWidth - clipMarginHeight;
const eleRight = eleLeft + eleRect.width + 2 * clipMarginWidth - scaledBorderLeftWidth - scaledBorderRightWidth - eleScrollWidth;
const eleBottom = eleTop + eleRect.height + 2 * clipMarginHeight - scaledBorderTopWidth - scaledBorderBottomWidth - eleScrollHeight;
visibleArea.left = Math.max(visibleArea.left, eleLeft);
visibleArea.top = Math.max(visibleArea.top, eleTop);
visibleArea.right = Math.min(visibleArea.right, eleRight);
visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom);
});
return visibleArea;
}
@@ -0,0 +1,9 @@
import * as React from 'react';
import type { AlignType, ArrowPos, ArrowTypeOuter } from '../interface';
export interface ArrowProps {
prefixCls: string;
align: AlignType;
arrow: ArrowTypeOuter;
arrowPos: ArrowPos;
}
export default function Arrow(props: ArrowProps): React.JSX.Element;
+72
View File
@@ -0,0 +1,72 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Arrow;
var _clsx = require("clsx");
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function Arrow(props) {
const {
prefixCls,
align,
arrow,
arrowPos
} = props;
const {
className,
content,
style
} = arrow || {};
const {
x = 0,
y = 0
} = arrowPos;
const arrowRef = React.useRef(null);
// Skip if no align
if (!align || !align.points) {
return null;
}
const alignStyle = {
position: 'absolute'
};
// Skip if no need to align
if (align.autoArrow !== false) {
const popupPoints = align.points[0];
const targetPoints = align.points[1];
const popupTB = popupPoints[0];
const popupLR = popupPoints[1];
const targetTB = targetPoints[0];
const targetLR = targetPoints[1];
// Top & Bottom
if (popupTB === targetTB || !['t', 'b'].includes(popupTB)) {
alignStyle.top = y;
} else if (popupTB === 't') {
alignStyle.top = 0;
} else {
alignStyle.bottom = 0;
}
// Left & Right
if (popupLR === targetLR || !['l', 'r'].includes(popupLR)) {
alignStyle.left = x;
} else if (popupLR === 'l') {
alignStyle.left = 0;
} else {
alignStyle.right = 0;
}
}
return /*#__PURE__*/React.createElement("div", {
ref: arrowRef,
className: (0, _clsx.clsx)(`${prefixCls}-arrow`, className),
style: {
...alignStyle,
...style
}
}, content);
}
+11
View File
@@ -0,0 +1,11 @@
import type { CSSMotionProps } from '@rc-component/motion';
import * as React from 'react';
export interface MaskProps {
prefixCls: string;
open?: boolean;
zIndex?: number;
mask?: boolean;
motion?: CSSMotionProps;
mobile?: boolean;
}
export default function Mask(props: MaskProps): React.JSX.Element;
+38
View File
@@ -0,0 +1,38 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = Mask;
var _clsx = require("clsx");
var _motion = _interopRequireDefault(require("@rc-component/motion"));
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
function Mask(props) {
const {
prefixCls,
open,
zIndex,
mask,
motion,
mobile
} = props;
if (!mask) {
return null;
}
return /*#__PURE__*/React.createElement(_motion.default, _extends({}, motion, {
motionAppear: true,
visible: open,
removeOnLeave: true
}), ({
className
}) => /*#__PURE__*/React.createElement("div", {
style: {
zIndex
},
className: (0, _clsx.clsx)(`${prefixCls}-mask`, mobile && `${prefixCls}-mobile-mask`, className)
}));
}
@@ -0,0 +1,7 @@
import * as React from 'react';
export interface PopupContentProps {
children?: React.ReactNode;
cache?: boolean;
}
declare const PopupContent: React.MemoExoticComponent<({ children }: PopupContentProps) => React.ReactElement<any, string | React.JSXElementConstructor<any>>>;
export default PopupContent;
@@ -0,0 +1,16 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const PopupContent = /*#__PURE__*/React.memo(({
children
}) => children, (_, next) => next.cache);
if (process.env.NODE_ENV !== 'production') {
PopupContent.displayName = 'PopupContent';
}
var _default = exports.default = PopupContent;
@@ -0,0 +1,57 @@
import type { CSSMotionProps } from '@rc-component/motion';
import { type ResizeObserverProps } from '@rc-component/resize-observer';
import * as React from 'react';
import type { TriggerProps } from '../';
import type { AlignType, ArrowPos, ArrowTypeOuter } from '../interface';
import type { PortalProps } from '@rc-component/portal';
export interface MobileConfig {
mask?: boolean;
/** Set popup motion. You can ref `rc-motion` for more info. */
motion?: CSSMotionProps;
/** Set mask motion. You can ref `rc-motion` for more info. */
maskMotion?: CSSMotionProps;
}
export interface PopupProps {
onEsc?: PortalProps['onEsc'];
prefixCls: string;
className?: string;
style?: React.CSSProperties;
popup?: TriggerProps['popup'];
target: HTMLElement;
onMouseEnter?: React.MouseEventHandler<HTMLDivElement>;
onMouseLeave?: React.MouseEventHandler<HTMLDivElement>;
onPointerEnter?: React.MouseEventHandler<HTMLDivElement>;
onPointerDownCapture?: React.MouseEventHandler<HTMLDivElement>;
zIndex?: number;
mask?: boolean;
onVisibleChanged: (visible: boolean) => void;
align?: AlignType;
arrow?: ArrowTypeOuter;
arrowPos: ArrowPos;
open: boolean;
/** Tell Portal that should keep in screen. e.g. should wait all motion end */
keepDom: boolean;
fresh?: boolean;
onClick?: React.MouseEventHandler<HTMLDivElement>;
motion?: CSSMotionProps;
maskMotion?: CSSMotionProps;
forceRender?: boolean;
getPopupContainer?: TriggerProps['getPopupContainer'];
autoDestroy?: boolean;
portal: React.ComponentType<any>;
children?: React.ReactElement;
ready: boolean;
offsetX: number;
offsetY: number;
offsetR: number;
offsetB: number;
onAlign: VoidFunction;
onPrepare: () => Promise<void>;
stretch?: string;
targetWidth?: number;
targetHeight?: number;
onResize?: ResizeObserverProps['onResize'];
mobile?: MobileConfig;
}
declare const Popup: React.ForwardRefExoticComponent<PopupProps & React.RefAttributes<HTMLDivElement>>;
export default Popup;
+200
View File
@@ -0,0 +1,200 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _clsx = require("clsx");
var _motion = _interopRequireDefault(require("@rc-component/motion"));
var _resizeObserver = _interopRequireDefault(require("@rc-component/resize-observer"));
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _ref = require("@rc-component/util/lib/ref");
var React = _interopRequireWildcard(require("react"));
var _Arrow = _interopRequireDefault(require("./Arrow"));
var _Mask = _interopRequireDefault(require("./Mask"));
var _PopupContent = _interopRequireDefault(require("./PopupContent"));
var _useOffsetStyle = _interopRequireDefault(require("../hooks/useOffsetStyle"));
var _util = require("@rc-component/util");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
const Popup = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
onEsc,
popup,
className,
prefixCls,
style,
target,
onVisibleChanged,
// Open
open,
keepDom,
fresh,
// Click
onClick,
// Mask
mask,
// Arrow
arrow,
arrowPos,
align,
// Motion
motion,
maskMotion,
// Mobile
mobile,
// Portal
forceRender,
getPopupContainer,
autoDestroy,
portal: Portal,
children,
zIndex,
onMouseEnter,
onMouseLeave,
onPointerEnter,
onPointerDownCapture,
ready,
offsetX,
offsetY,
offsetR,
offsetB,
onAlign,
onPrepare,
// Resize
onResize,
stretch,
targetWidth,
targetHeight
} = props;
const popupContent = typeof popup === 'function' ? popup() : popup;
// We can not remove holder only when motion finished.
const isNodeVisible = open || keepDom;
// ========================= Mobile =========================
const isMobile = !!mobile;
// ========================== Mask ==========================
const [mergedMask, mergedMaskMotion, mergedPopupMotion] = React.useMemo(() => {
if (mobile) {
return [mobile.mask, mobile.maskMotion, mobile.motion];
}
return [mask, maskMotion, motion];
}, [mobile, mask, maskMotion, motion]);
// ======================= Container ========================
const getPopupContainerNeedParams = getPopupContainer?.length > 0;
const [show, setShow] = React.useState(!getPopupContainer || !getPopupContainerNeedParams);
// Delay to show since `getPopupContainer` need target element
(0, _useLayoutEffect.default)(() => {
if (!show && getPopupContainerNeedParams && target) {
setShow(true);
}
}, [show, getPopupContainerNeedParams, target]);
// ========================= Resize =========================
const onInternalResize = (0, _util.useEvent)((size, ele) => {
onResize?.(size, ele);
onAlign();
});
// ========================= Styles =========================
const offsetStyle = (0, _useOffsetStyle.default)(isMobile, ready, open, align, offsetR, offsetB, offsetX, offsetY);
// ========================= Render =========================
if (!show) {
return null;
}
// >>>>> Misc
const miscStyle = {};
if (stretch) {
if (stretch.includes('height') && targetHeight) {
miscStyle.height = targetHeight;
} else if (stretch.includes('minHeight') && targetHeight) {
miscStyle.minHeight = targetHeight;
}
if (stretch.includes('width') && targetWidth) {
miscStyle.width = targetWidth;
} else if (stretch.includes('minWidth') && targetWidth) {
miscStyle.minWidth = targetWidth;
}
}
if (!open) {
miscStyle.pointerEvents = 'none';
}
return /*#__PURE__*/React.createElement(Portal, {
open: forceRender || isNodeVisible,
getContainer: getPopupContainer && (() => getPopupContainer(target)),
autoDestroy: autoDestroy,
onEsc: onEsc
}, /*#__PURE__*/React.createElement(_Mask.default, {
prefixCls: prefixCls,
open: open,
zIndex: zIndex,
mask: mergedMask,
motion: mergedMaskMotion,
mobile: isMobile
}), /*#__PURE__*/React.createElement(_resizeObserver.default, {
onResize: onInternalResize,
disabled: !open
}, resizeObserverRef => {
return /*#__PURE__*/React.createElement(_motion.default, _extends({
motionAppear: true,
motionEnter: true,
motionLeave: true,
removeOnLeave: false,
forceRender: forceRender,
leavedClassName: `${prefixCls}-hidden`
}, mergedPopupMotion, {
onAppearPrepare: onPrepare,
onEnterPrepare: onPrepare,
visible: open,
onVisibleChanged: nextVisible => {
motion?.onVisibleChanged?.(nextVisible);
onVisibleChanged(nextVisible);
}
}), ({
className: motionClassName,
style: motionStyle
}, motionRef) => {
const cls = (0, _clsx.clsx)(prefixCls, motionClassName, className, {
[`${prefixCls}-mobile`]: isMobile
});
return /*#__PURE__*/React.createElement("div", {
ref: (0, _ref.composeRef)(resizeObserverRef, ref, motionRef),
className: cls,
style: {
'--arrow-x': `${arrowPos.x || 0}px`,
'--arrow-y': `${arrowPos.y || 0}px`,
...offsetStyle,
...miscStyle,
...motionStyle,
boxSizing: 'border-box',
zIndex,
...style
},
onMouseEnter: onMouseEnter,
onMouseLeave: onMouseLeave,
onPointerEnter: onPointerEnter,
onClick: onClick,
onPointerDownCapture: onPointerDownCapture
}, arrow && /*#__PURE__*/React.createElement(_Arrow.default, {
prefixCls: prefixCls,
arrow: arrow,
arrowPos: arrowPos,
align: align
}), /*#__PURE__*/React.createElement(_PopupContent.default, {
cache: !open && !fresh
}, popupContent));
});
}), children);
});
if (process.env.NODE_ENV !== 'production') {
Popup.displayName = 'Popup';
}
var _default = exports.default = Popup;
@@ -0,0 +1,24 @@
import React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { AlignType, ArrowPos } from '../interface';
export interface UniqueContainerProps {
prefixCls: string;
isMobile: boolean;
ready: boolean;
open: boolean;
align: AlignType;
offsetR: number;
offsetB: number;
offsetX: number;
offsetY: number;
arrowPos?: ArrowPos;
popupSize?: {
width: number;
height: number;
};
motion?: CSSMotionProps;
uniqueContainerClassName?: string;
uniqueContainerStyle?: React.CSSProperties;
}
declare const UniqueContainer: (props: UniqueContainerProps) => React.JSX.Element;
export default UniqueContainer;
@@ -0,0 +1,83 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = _interopRequireDefault(require("react"));
var _useOffsetStyle = _interopRequireDefault(require("../hooks/useOffsetStyle"));
var _clsx = require("clsx");
var _motion = _interopRequireDefault(require("@rc-component/motion"));
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
const UniqueContainer = props => {
const {
prefixCls,
isMobile,
ready,
open,
align,
offsetR,
offsetB,
offsetX,
offsetY,
arrowPos,
popupSize,
motion,
uniqueContainerClassName,
uniqueContainerStyle
} = props;
const containerCls = `${prefixCls}-unique-container`;
const [motionVisible, setMotionVisible] = _react.default.useState(false);
// ========================= Styles =========================
const offsetStyle = (0, _useOffsetStyle.default)(isMobile, ready, open, align, offsetR, offsetB, offsetX, offsetY);
// Cache for offsetStyle when ready is true
const cachedOffsetStyleRef = _react.default.useRef(offsetStyle);
// Update cached offset style when ready is true
if (ready) {
cachedOffsetStyleRef.current = offsetStyle;
}
// Apply popup size if available
const sizeStyle = {};
if (popupSize) {
sizeStyle.width = popupSize.width;
sizeStyle.height = popupSize.height;
}
// ========================= Render =========================
return /*#__PURE__*/_react.default.createElement(_motion.default, _extends({
motionAppear: true,
motionEnter: true,
motionLeave: true,
removeOnLeave: false,
leavedClassName: `${containerCls}-hidden`
}, motion, {
visible: open,
onVisibleChanged: nextVisible => {
setMotionVisible(nextVisible);
}
}), ({
className: motionClassName,
style: motionStyle
}) => {
const cls = (0, _clsx.clsx)(containerCls, motionClassName, uniqueContainerClassName, {
[`${containerCls}-visible`]: motionVisible
});
return /*#__PURE__*/_react.default.createElement("div", {
className: cls,
style: {
'--arrow-x': `${arrowPos?.x || 0}px`,
'--arrow-y': `${arrowPos?.y || 0}px`,
...cachedOffsetStyleRef.current,
...sizeStyle,
...motionStyle,
...uniqueContainerStyle
}
});
});
};
var _default = exports.default = UniqueContainer;
@@ -0,0 +1,9 @@
import * as React from 'react';
import { type UniqueShowOptions } from '../context';
export interface UniqueProviderProps {
children: React.ReactNode;
/** Additional handle options data to do the customize info */
postTriggerProps?: (options: UniqueShowOptions) => UniqueShowOptions;
}
declare const UniqueProvider: ({ children, postTriggerProps, }: UniqueProviderProps) => React.JSX.Element;
export default UniqueProvider;
@@ -0,0 +1,184 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _portal = _interopRequireDefault(require("@rc-component/portal"));
var _context = _interopRequireWildcard(require("../context"));
var _useDelay = _interopRequireDefault(require("../hooks/useDelay"));
var _useAlign = _interopRequireDefault(require("../hooks/useAlign"));
var _Popup = _interopRequireDefault(require("../Popup"));
var _util = require("@rc-component/util");
var _useTargetState = _interopRequireDefault(require("./useTargetState"));
var _findDOMNode = require("@rc-component/util/lib/Dom/findDOMNode");
var _UniqueContainer = _interopRequireDefault(require("./UniqueContainer"));
var _clsx = require("clsx");
var _util2 = require("../util");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const UniqueProvider = ({
children,
postTriggerProps
}) => {
const [trigger, open, options, onTargetVisibleChanged] = (0, _useTargetState.default)();
// ========================== Options ===========================
const mergedOptions = React.useMemo(() => {
if (!options || !postTriggerProps) {
return options;
}
return postTriggerProps(options);
}, [options, postTriggerProps]);
// =========================== Popup ============================
const [popupEle, setPopupEle] = React.useState(null);
const [popupSize, setPopupSize] = React.useState(null);
// Used for forwardRef popup. Not use internal
const externalPopupRef = React.useRef(null);
const setPopupRef = (0, _util.useEvent)(node => {
externalPopupRef.current = node;
if ((0, _findDOMNode.isDOM)(node) && popupEle !== node) {
setPopupEle(node);
}
});
// ========================== Register ==========================
// Store the isOpen function from the latest show call
const isOpenRef = React.useRef(null);
const delayInvoke = (0, _useDelay.default)();
const show = (0, _util.useEvent)((showOptions, isOpen) => {
// Store the isOpen function for later use in hide
isOpenRef.current = isOpen;
delayInvoke(() => {
trigger(showOptions);
}, showOptions.delay);
});
const hide = delay => {
delayInvoke(() => {
// Check if we should still hide by calling the isOpen function
// If isOpen returns true, it means another trigger wants to keep it open
if (isOpenRef.current?.()) {
return; // Don't hide if something else wants it open
}
trigger(false);
// Don't clear target, currentNode, options immediately, wait until animation completes
}, delay);
};
// Callback after animation completes
const onVisibleChanged = (0, _util.useEvent)(visible => {
// Call useTargetState callback to handle animation state
onTargetVisibleChanged(visible);
});
// =========================== Align ============================
const [ready, offsetX, offsetY, offsetR, offsetB, arrowX, arrowY,
// scaleX - not used in UniqueProvider
,,
// scaleY - not used in UniqueProvider
alignInfo, onAlign] = (0, _useAlign.default)(open, popupEle, mergedOptions?.target, mergedOptions?.popupPlacement, mergedOptions?.builtinPlacements || {}, mergedOptions?.popupAlign, undefined,
// onPopupAlign
false // isMobile
);
const alignedClassName = React.useMemo(() => {
if (!mergedOptions) {
return '';
}
const baseClassName = (0, _util2.getAlignPopupClassName)(mergedOptions.builtinPlacements || {}, mergedOptions.prefixCls || '', alignInfo, false // alignPoint is false for UniqueProvider
);
return (0, _clsx.clsx)(baseClassName, mergedOptions.getPopupClassNameFromAlign?.(alignInfo));
}, [alignInfo, mergedOptions?.getPopupClassNameFromAlign, mergedOptions?.builtinPlacements, mergedOptions?.prefixCls]);
const contextValue = React.useMemo(() => ({
show,
hide
}), []);
// =========================== Align ============================
React.useEffect(() => {
onAlign();
}, [mergedOptions?.target]);
// =========================== Motion ===========================
const onPrepare = (0, _util.useEvent)(() => {
onAlign();
return Promise.resolve();
});
// ======================== Trigger Context =====================
const subPopupElements = React.useRef({});
const parentContext = React.useContext(_context.default);
const triggerContextValue = React.useMemo(() => ({
registerSubPopup: (id, subPopupEle) => {
subPopupElements.current[id] = subPopupEle;
parentContext?.registerSubPopup(id, subPopupEle);
}
}), [parentContext]);
// =========================== Render ===========================
const prefixCls = mergedOptions?.prefixCls;
return /*#__PURE__*/React.createElement(_context.UniqueContext.Provider, {
value: contextValue
}, children, mergedOptions && /*#__PURE__*/React.createElement(_context.default.Provider, {
value: triggerContextValue
}, /*#__PURE__*/React.createElement(_Popup.default, {
ref: setPopupRef,
portal: _portal.default,
onEsc: mergedOptions.onEsc,
prefixCls: prefixCls,
popup: mergedOptions.popup,
className: (0, _clsx.clsx)(mergedOptions.popupClassName, alignedClassName, `${prefixCls}-unique-controlled`),
style: mergedOptions.popupStyle,
target: mergedOptions.target,
open: open,
keepDom: true,
fresh: true,
autoDestroy: false,
onVisibleChanged: onVisibleChanged,
ready: ready,
offsetX: offsetX,
offsetY: offsetY,
offsetR: offsetR,
offsetB: offsetB,
onAlign: onAlign,
onPrepare: onPrepare,
onResize: size => setPopupSize({
width: size.offsetWidth,
height: size.offsetHeight
}),
arrowPos: {
x: arrowX,
y: arrowY
},
align: alignInfo,
zIndex: mergedOptions.zIndex,
mask: mergedOptions.mask,
arrow: mergedOptions.arrow,
motion: mergedOptions.popupMotion,
maskMotion: mergedOptions.maskMotion,
getPopupContainer: mergedOptions.getPopupContainer
}, /*#__PURE__*/React.createElement(_UniqueContainer.default, {
prefixCls: prefixCls,
isMobile: false,
ready: ready,
open: open,
align: alignInfo,
offsetR: offsetR,
offsetB: offsetB,
offsetX: offsetX,
offsetY: offsetY,
arrowPos: {
x: arrowX,
y: arrowY
},
popupSize: popupSize,
motion: mergedOptions.popupMotion,
uniqueContainerClassName: (0, _clsx.clsx)(mergedOptions.uniqueContainerClassName, alignedClassName),
uniqueContainerStyle: mergedOptions.uniqueContainerStyle
}))));
};
var _default = exports.default = UniqueProvider;
@@ -0,0 +1,16 @@
import type { UniqueShowOptions } from '../context';
/**
* Control the state of popup bind target:
* 1. When set `target`. Do show the popup.
* 2. When `target` is removed. Do hide the popup.
* 3. When `target` change to another one:
* a. We wait motion finish of previous popup.
* b. Then we set new target and show the popup.
* 4. During appear/enter animation, cache new options and apply after animation completes.
*/
export default function useTargetState(): [
trigger: (options: UniqueShowOptions | false) => void,
open: boolean,
cacheOptions: UniqueShowOptions | null,
onVisibleChanged: (visible: boolean) => void
];
@@ -0,0 +1,62 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useTargetState;
var _react = _interopRequireDefault(require("react"));
var _util = require("@rc-component/util");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
/**
* Control the state of popup bind target:
* 1. When set `target`. Do show the popup.
* 2. When `target` is removed. Do hide the popup.
* 3. When `target` change to another one:
* a. We wait motion finish of previous popup.
* b. Then we set new target and show the popup.
* 4. During appear/enter animation, cache new options and apply after animation completes.
*/
function useTargetState() {
const [options, setOptions] = _react.default.useState(null);
const [open, setOpen] = _react.default.useState(false);
const [isAnimating, setIsAnimating] = _react.default.useState(false);
const pendingOptionsRef = _react.default.useRef(null);
const trigger = (0, _util.useEvent)(nextOptions => {
if (nextOptions === false) {
// Clear pending options when hiding
pendingOptionsRef.current = null;
setOpen(false);
} else {
if (isAnimating && open) {
// If animating (appear or enter), cache new options
pendingOptionsRef.current = nextOptions;
} else {
setOpen(true);
// Set new options
setOptions(nextOptions);
pendingOptionsRef.current = null;
// Only mark as animating when transitioning from closed to open
if (!open) {
setIsAnimating(true);
}
}
}
});
const onVisibleChanged = (0, _util.useEvent)(visible => {
if (visible) {
// Animation enter completed, check if there are pending options
setIsAnimating(false);
if (pendingOptionsRef.current) {
// Apply pending options
setOptions(pendingOptionsRef.current);
pendingOptionsRef.current = null;
}
} else {
// Animation leave completed
setIsAnimating(false);
pendingOptionsRef.current = null;
}
});
return [trigger, open, options, onVisibleChanged];
}
+38
View File
@@ -0,0 +1,38 @@
import * as React from 'react';
import type { CSSMotionProps } from '@rc-component/motion';
import type { PortalProps } from '@rc-component/portal';
import type { TriggerProps } from './index';
import type { AlignType, ArrowTypeOuter, BuildInPlacements } from './interface';
export interface TriggerContextProps {
registerSubPopup: (id: string, node: HTMLElement) => void;
}
declare const TriggerContext: React.Context<TriggerContextProps>;
export default TriggerContext;
export interface UniqueShowOptions {
id: string;
popup: TriggerProps['popup'];
target: HTMLElement;
delay: number;
prefixCls?: string;
popupClassName?: string;
uniqueContainerClassName?: string;
uniqueContainerStyle?: React.CSSProperties;
popupStyle?: React.CSSProperties;
popupPlacement?: string;
builtinPlacements?: BuildInPlacements;
popupAlign?: AlignType;
zIndex?: number;
mask?: boolean;
maskClosable?: boolean;
popupMotion?: CSSMotionProps;
maskMotion?: CSSMotionProps;
arrow?: ArrowTypeOuter;
getPopupContainer?: TriggerProps['getPopupContainer'];
getPopupClassNameFromAlign?: (align: AlignType) => string;
onEsc?: PortalProps['onEsc'];
}
export interface UniqueContextProps {
show: (options: UniqueShowOptions, isOpen: () => boolean) => void;
hide: (delay: number) => void;
}
export declare const UniqueContext: React.Context<UniqueContextProps>;
+14
View File
@@ -0,0 +1,14 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = exports.UniqueContext = void 0;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
// ===================== Nest =====================
const TriggerContext = /*#__PURE__*/React.createContext(null);
var _default = exports.default = TriggerContext; // ==================== Unique ====================
const UniqueContext = exports.UniqueContext = /*#__PURE__*/React.createContext(null);
@@ -0,0 +1,5 @@
import type { ActionType } from '../interface';
type InternalActionType = ActionType | 'touch';
type ActionTypes = InternalActionType | InternalActionType[];
export default function useAction(action: ActionTypes, showAction?: ActionTypes, hideAction?: ActionTypes): [showAction: Set<InternalActionType>, hideAction: Set<InternalActionType>];
export {};
@@ -0,0 +1,27 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useAction;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function toArray(val) {
return val ? Array.isArray(val) ? val : [val] : [];
}
function useAction(action, showAction, hideAction) {
return React.useMemo(() => {
const mergedShowAction = toArray(showAction ?? action);
const mergedHideAction = toArray(hideAction ?? action);
const showActionSet = new Set(mergedShowAction);
const hideActionSet = new Set(mergedHideAction);
if (showActionSet.has('hover') && !showActionSet.has('click')) {
showActionSet.add('touch');
}
if (hideActionSet.has('hover') && !hideActionSet.has('click')) {
hideActionSet.add('touch');
}
return [showActionSet, hideActionSet];
}, [action, showAction, hideAction]);
}
@@ -0,0 +1,15 @@
import type { TriggerProps } from '..';
import type { AlignType } from '../interface';
export default function useAlign(open: boolean, popupEle: HTMLElement, target: HTMLElement | [x: number, y: number], placement: string, builtinPlacements: any, popupAlign?: AlignType, onPopupAlign?: TriggerProps['onPopupAlign'], mobile?: boolean): [
ready: boolean,
offsetX: number,
offsetY: number,
offsetR: number,
offsetB: number,
arrowX: number,
arrowY: number,
scaleX: number,
scaleY: number,
align: AlignType,
onAlign: VoidFunction
];
@@ -0,0 +1,540 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useAlign;
var _findDOMNode = require("@rc-component/util/lib/Dom/findDOMNode");
var _isVisible = _interopRequireDefault(require("@rc-component/util/lib/Dom/isVisible"));
var _useEvent = _interopRequireDefault(require("@rc-component/util/lib/hooks/useEvent"));
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var React = _interopRequireWildcard(require("react"));
var _util = require("../util");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function getUnitOffset(size, offset = 0) {
const offsetStr = `${offset}`;
const cells = offsetStr.match(/^(.*)\%$/);
if (cells) {
return size * (parseFloat(cells[1]) / 100);
}
return parseFloat(offsetStr);
}
function getNumberOffset(rect, offset) {
const [offsetX, offsetY] = offset || [];
return [getUnitOffset(rect.width, offsetX), getUnitOffset(rect.height, offsetY)];
}
function splitPoints(points = '') {
return [points[0], points[1]];
}
function getAlignPoint(rect, points) {
const topBottom = points[0];
const leftRight = points[1];
let x;
let y;
// Top & Bottom
if (topBottom === 't') {
y = rect.y;
} else if (topBottom === 'b') {
y = rect.y + rect.height;
} else {
y = rect.y + rect.height / 2;
}
// Left & Right
if (leftRight === 'l') {
x = rect.x;
} else if (leftRight === 'r') {
x = rect.x + rect.width;
} else {
x = rect.x + rect.width / 2;
}
return {
x,
y
};
}
function reversePoints(points, index) {
const reverseMap = {
t: 'b',
b: 't',
l: 'r',
r: 'l'
};
const clone = [...points];
clone[index] = reverseMap[points[index]] || 'c';
return clone;
}
function flatPoints(points) {
return points.join('');
}
function useAlign(open, popupEle, target, placement, builtinPlacements, popupAlign, onPopupAlign, mobile) {
const [offsetInfo, setOffsetInfo] = React.useState({
ready: false,
offsetX: 0,
offsetY: 0,
offsetR: 0,
offsetB: 0,
arrowX: 0,
arrowY: 0,
scaleX: 1,
scaleY: 1,
align: builtinPlacements[placement] || {}
});
const alignCountRef = React.useRef(0);
const scrollerList = React.useMemo(() => {
if (!popupEle || mobile) {
return [];
}
return (0, _util.collectScroller)(popupEle);
}, [popupEle]);
// ========================= Flip ==========================
// We will memo flip info.
// If size change to make flip, it will memo the flip info and use it in next align.
const prevFlipRef = React.useRef({});
const resetFlipCache = () => {
prevFlipRef.current = {};
};
if (!open) {
resetFlipCache();
}
// ========================= Align =========================
const onAlign = (0, _useEvent.default)(() => {
if (popupEle && target && open && !mobile) {
const popupElement = popupEle;
const doc = popupElement.ownerDocument;
const win = (0, _util.getWin)(popupElement);
const {
position: popupPosition
} = win.getComputedStyle(popupElement);
const originLeft = popupElement.style.left;
const originTop = popupElement.style.top;
const originRight = popupElement.style.right;
const originBottom = popupElement.style.bottom;
const originOverflow = popupElement.style.overflow;
// Placement
const placementInfo = {
...builtinPlacements[placement],
...popupAlign
};
// placeholder element
const placeholderElement = doc.createElement('div');
popupElement.parentElement?.appendChild(placeholderElement);
placeholderElement.style.left = `${popupElement.offsetLeft}px`;
placeholderElement.style.top = `${popupElement.offsetTop}px`;
placeholderElement.style.position = popupPosition;
placeholderElement.style.height = `${popupElement.offsetHeight}px`;
placeholderElement.style.width = `${popupElement.offsetWidth}px`;
// Reset first
popupElement.style.left = '0';
popupElement.style.top = '0';
popupElement.style.right = 'auto';
popupElement.style.bottom = 'auto';
popupElement.style.overflow = 'hidden';
// Calculate align style, we should consider `transform` case
let targetRect;
if (Array.isArray(target)) {
targetRect = {
x: target[0],
y: target[1],
width: 0,
height: 0
};
} else {
const rect = target.getBoundingClientRect();
rect.x = rect.x ?? rect.left;
rect.y = rect.y ?? rect.top;
targetRect = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
};
}
const popupRect = popupElement.getBoundingClientRect();
const {
height,
width
} = win.getComputedStyle(popupElement);
popupRect.x = popupRect.x ?? popupRect.left;
popupRect.y = popupRect.y ?? popupRect.top;
const {
clientWidth,
clientHeight,
scrollWidth,
scrollHeight,
scrollTop,
scrollLeft
} = doc.documentElement;
const popupHeight = popupRect.height;
const popupWidth = popupRect.width;
const targetHeight = targetRect.height;
const targetWidth = targetRect.width;
// Get bounding of visible area
const visibleRegion = {
left: 0,
top: 0,
right: clientWidth,
bottom: clientHeight
};
const scrollRegion = {
left: -scrollLeft,
top: -scrollTop,
right: scrollWidth - scrollLeft,
bottom: scrollHeight - scrollTop
};
let {
htmlRegion
} = placementInfo;
const VISIBLE = 'visible';
const VISIBLE_FIRST = 'visibleFirst';
if (htmlRegion !== 'scroll' && htmlRegion !== VISIBLE_FIRST) {
htmlRegion = VISIBLE;
}
const isVisibleFirst = htmlRegion === VISIBLE_FIRST;
const scrollRegionArea = (0, _util.getVisibleArea)(scrollRegion, scrollerList);
const visibleRegionArea = (0, _util.getVisibleArea)(visibleRegion, scrollerList);
const visibleArea = htmlRegion === VISIBLE ? visibleRegionArea : scrollRegionArea;
// When set to `visibleFirst`,
// the check `adjust` logic will use `visibleRegion` for check first.
const adjustCheckVisibleArea = isVisibleFirst ? visibleRegionArea : visibleArea;
// Record right & bottom align data
popupElement.style.left = 'auto';
popupElement.style.top = 'auto';
popupElement.style.right = '0';
popupElement.style.bottom = '0';
const popupMirrorRect = popupElement.getBoundingClientRect();
// Reset back
popupElement.style.left = originLeft;
popupElement.style.top = originTop;
popupElement.style.right = originRight;
popupElement.style.bottom = originBottom;
popupElement.style.overflow = originOverflow;
popupElement.parentElement?.removeChild(placeholderElement);
// Calculate scale
const scaleX = (0, _util.toNum)(Math.round(popupWidth / parseFloat(width) * 1000) / 1000);
const scaleY = (0, _util.toNum)(Math.round(popupHeight / parseFloat(height) * 1000) / 1000);
// No need to align since it's not visible in view
if (scaleX === 0 || scaleY === 0 || (0, _findDOMNode.isDOM)(target) && !(0, _isVisible.default)(target)) {
return;
}
// Offset
const {
offset,
targetOffset
} = placementInfo;
let [popupOffsetX, popupOffsetY] = getNumberOffset(popupRect, offset);
const [targetOffsetX, targetOffsetY] = getNumberOffset(targetRect, targetOffset);
targetRect.x -= targetOffsetX;
targetRect.y -= targetOffsetY;
// Points
const [popupPoint, targetPoint] = placementInfo.points || [];
const targetPoints = splitPoints(targetPoint);
const popupPoints = splitPoints(popupPoint);
const targetAlignPoint = getAlignPoint(targetRect, targetPoints);
const popupAlignPoint = getAlignPoint(popupRect, popupPoints);
// Real align info may not same as origin one
const nextAlignInfo = {
...placementInfo
};
let nextPoints = [popupPoints, targetPoints];
// Next Offset
let nextOffsetX = targetAlignPoint.x - popupAlignPoint.x + popupOffsetX;
let nextOffsetY = targetAlignPoint.y - popupAlignPoint.y + popupOffsetY;
// ============== Intersection ===============
// Get area by position. Used for check if flip area is better
function getIntersectionVisibleArea(offsetX, offsetY, area = visibleArea) {
const l = popupRect.x + offsetX;
const t = popupRect.y + offsetY;
const r = l + popupWidth;
const b = t + popupHeight;
const visibleL = Math.max(l, area.left);
const visibleT = Math.max(t, area.top);
const visibleR = Math.min(r, area.right);
const visibleB = Math.min(b, area.bottom);
return Math.max(0, (visibleR - visibleL) * (visibleB - visibleT));
}
const originIntersectionVisibleArea = getIntersectionVisibleArea(nextOffsetX, nextOffsetY);
// As `visibleFirst`, we prepare this for check
const originIntersectionRecommendArea = getIntersectionVisibleArea(nextOffsetX, nextOffsetY, visibleRegionArea);
// ========================== Overflow ===========================
const targetAlignPointTL = getAlignPoint(targetRect, ['t', 'l']);
const popupAlignPointTL = getAlignPoint(popupRect, ['t', 'l']);
const targetAlignPointBR = getAlignPoint(targetRect, ['b', 'r']);
const popupAlignPointBR = getAlignPoint(popupRect, ['b', 'r']);
const overflow = placementInfo.overflow || {};
const {
adjustX,
adjustY,
shiftX,
shiftY
} = overflow;
const supportAdjust = val => {
if (typeof val === 'boolean') {
return val;
}
return val >= 0;
};
// Prepare position
let nextPopupY;
let nextPopupBottom;
let nextPopupX;
let nextPopupRight;
function syncNextPopupPosition() {
nextPopupY = popupRect.y + nextOffsetY;
nextPopupBottom = nextPopupY + popupHeight;
nextPopupX = popupRect.x + nextOffsetX;
nextPopupRight = nextPopupX + popupWidth;
}
syncNextPopupPosition();
// >>>>>>>>>> Top & Bottom
const needAdjustY = supportAdjust(adjustY);
const sameTB = popupPoints[0] === targetPoints[0];
// Bottom to Top
if (needAdjustY && popupPoints[0] === 't' && (nextPopupBottom > adjustCheckVisibleArea.bottom || prevFlipRef.current.bt)) {
let tmpNextOffsetY = nextOffsetY;
if (sameTB) {
tmpNextOffsetY -= popupHeight - targetHeight;
} else {
tmpNextOffsetY = targetAlignPointTL.y - popupAlignPointBR.y - popupOffsetY;
}
const newVisibleArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.bt = true;
nextOffsetY = tmpNextOffsetY;
popupOffsetY = -popupOffsetY;
nextPoints = [reversePoints(nextPoints[0], 0), reversePoints(nextPoints[1], 0)];
} else {
prevFlipRef.current.bt = false;
}
}
// Top to Bottom
if (needAdjustY && popupPoints[0] === 'b' && (nextPopupY < adjustCheckVisibleArea.top || prevFlipRef.current.tb)) {
let tmpNextOffsetY = nextOffsetY;
if (sameTB) {
tmpNextOffsetY += popupHeight - targetHeight;
} else {
tmpNextOffsetY = targetAlignPointBR.y - popupAlignPointTL.y - popupOffsetY;
}
const newVisibleArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(nextOffsetX, tmpNextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.tb = true;
nextOffsetY = tmpNextOffsetY;
popupOffsetY = -popupOffsetY;
nextPoints = [reversePoints(nextPoints[0], 0), reversePoints(nextPoints[1], 0)];
} else {
prevFlipRef.current.tb = false;
}
}
// >>>>>>>>>> Left & Right
const needAdjustX = supportAdjust(adjustX);
// >>>>> Flip
const sameLR = popupPoints[1] === targetPoints[1];
// Right to Left
if (needAdjustX && popupPoints[1] === 'l' && (nextPopupRight > adjustCheckVisibleArea.right || prevFlipRef.current.rl)) {
let tmpNextOffsetX = nextOffsetX;
if (sameLR) {
tmpNextOffsetX -= popupWidth - targetWidth;
} else {
tmpNextOffsetX = targetAlignPointTL.x - popupAlignPointBR.x - popupOffsetX;
}
const newVisibleArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.rl = true;
nextOffsetX = tmpNextOffsetX;
popupOffsetX = -popupOffsetX;
nextPoints = [reversePoints(nextPoints[0], 1), reversePoints(nextPoints[1], 1)];
} else {
prevFlipRef.current.rl = false;
}
}
// Left to Right
if (needAdjustX && popupPoints[1] === 'r' && (nextPopupX < adjustCheckVisibleArea.left || prevFlipRef.current.lr)) {
let tmpNextOffsetX = nextOffsetX;
if (sameLR) {
tmpNextOffsetX += popupWidth - targetWidth;
} else {
tmpNextOffsetX = targetAlignPointBR.x - popupAlignPointTL.x - popupOffsetX;
}
const newVisibleArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY);
const newVisibleRecommendArea = getIntersectionVisibleArea(tmpNextOffsetX, nextOffsetY, visibleRegionArea);
if (
// Of course use larger one
newVisibleArea > originIntersectionVisibleArea || newVisibleArea === originIntersectionVisibleArea && (!isVisibleFirst ||
// Choose recommend one
newVisibleRecommendArea >= originIntersectionRecommendArea)) {
prevFlipRef.current.lr = true;
nextOffsetX = tmpNextOffsetX;
popupOffsetX = -popupOffsetX;
nextPoints = [reversePoints(nextPoints[0], 1), reversePoints(nextPoints[1], 1)];
} else {
prevFlipRef.current.lr = false;
}
}
nextAlignInfo.points = [flatPoints(nextPoints[0]), flatPoints(nextPoints[1])];
// ============================ Shift ============================
syncNextPopupPosition();
const numShiftX = shiftX === true ? 0 : shiftX;
if (typeof numShiftX === 'number') {
// Left
if (nextPopupX < visibleRegionArea.left) {
nextOffsetX -= nextPopupX - visibleRegionArea.left - popupOffsetX;
if (targetRect.x + targetWidth < visibleRegionArea.left + numShiftX) {
nextOffsetX += targetRect.x - visibleRegionArea.left + targetWidth - numShiftX;
}
}
// Right
if (nextPopupRight > visibleRegionArea.right) {
nextOffsetX -= nextPopupRight - visibleRegionArea.right - popupOffsetX;
if (targetRect.x > visibleRegionArea.right - numShiftX) {
nextOffsetX += targetRect.x - visibleRegionArea.right + numShiftX;
}
}
}
const numShiftY = shiftY === true ? 0 : shiftY;
if (typeof numShiftY === 'number') {
// Top
if (nextPopupY < visibleRegionArea.top) {
nextOffsetY -= nextPopupY - visibleRegionArea.top - popupOffsetY;
// When target if far away from visible area
// Stop shift
if (targetRect.y + targetHeight < visibleRegionArea.top + numShiftY) {
nextOffsetY += targetRect.y - visibleRegionArea.top + targetHeight - numShiftY;
}
}
// Bottom
if (nextPopupBottom > visibleRegionArea.bottom) {
nextOffsetY -= nextPopupBottom - visibleRegionArea.bottom - popupOffsetY;
if (targetRect.y > visibleRegionArea.bottom - numShiftY) {
nextOffsetY += targetRect.y - visibleRegionArea.bottom + numShiftY;
}
}
}
// ============================ Arrow ============================
// Arrow center align
const popupLeft = popupRect.x + nextOffsetX;
const popupRight = popupLeft + popupWidth;
const popupTop = popupRect.y + nextOffsetY;
const popupBottom = popupTop + popupHeight;
const targetLeft = targetRect.x;
const targetRight = targetLeft + targetWidth;
const targetTop = targetRect.y;
const targetBottom = targetTop + targetHeight;
/** Max left of the popup and target element */
const maxLeft = Math.max(popupLeft, targetLeft);
/** Min right of the popup and target element */
const minRight = Math.min(popupRight, targetRight);
/** The center X of popup & target cross area */
const xCenter = (maxLeft + minRight) / 2;
/** Arrow X of popup offset */
const nextArrowX = xCenter - popupLeft;
const maxTop = Math.max(popupTop, targetTop);
const minBottom = Math.min(popupBottom, targetBottom);
const yCenter = (maxTop + minBottom) / 2;
const nextArrowY = yCenter - popupTop;
onPopupAlign?.(popupEle, nextAlignInfo);
// Additional calculate right & bottom position
let offsetX4Right = popupMirrorRect.right - popupRect.x - (nextOffsetX + popupRect.width);
let offsetY4Bottom = popupMirrorRect.bottom - popupRect.y - (nextOffsetY + popupRect.height);
if (scaleX === 1) {
nextOffsetX = Math.floor(nextOffsetX);
offsetX4Right = Math.floor(offsetX4Right);
}
if (scaleY === 1) {
nextOffsetY = Math.floor(nextOffsetY);
offsetY4Bottom = Math.floor(offsetY4Bottom);
}
const nextOffsetInfo = {
ready: true,
offsetX: nextOffsetX / scaleX,
offsetY: nextOffsetY / scaleY,
offsetR: offsetX4Right / scaleX,
offsetB: offsetY4Bottom / scaleY,
arrowX: nextArrowX / scaleX,
arrowY: nextArrowY / scaleY,
scaleX,
scaleY,
align: nextAlignInfo
};
setOffsetInfo(nextOffsetInfo);
}
});
const triggerAlign = () => {
alignCountRef.current += 1;
const id = alignCountRef.current;
// Merge all align requirement into one frame
Promise.resolve().then(() => {
if (alignCountRef.current === id) {
onAlign();
}
});
};
// Reset ready status when placement & open changed
const resetReady = () => {
setOffsetInfo(ori => ({
...ori,
ready: false
}));
};
(0, _useLayoutEffect.default)(resetReady, [placement]);
(0, _useLayoutEffect.default)(() => {
if (!open) {
resetReady();
}
}, [open]);
return [offsetInfo.ready, offsetInfo.offsetX, offsetInfo.offsetY, offsetInfo.offsetR, offsetInfo.offsetB, offsetInfo.arrowX, offsetInfo.arrowY, offsetInfo.scaleX, offsetInfo.scaleY, offsetInfo.align, triggerAlign];
}
@@ -0,0 +1 @@
export default function useDelay(): (callback: VoidFunction, delay: number) => void;
@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useDelay;
var React = _interopRequireWildcard(require("react"));
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function useDelay() {
const delayRef = React.useRef(null);
const clearDelay = () => {
if (delayRef.current) {
clearTimeout(delayRef.current);
delayRef.current = null;
}
};
const delayInvoke = (callback, delay) => {
clearDelay();
if (delay === 0) {
callback();
} else {
delayRef.current = setTimeout(() => {
callback();
}, delay * 1000);
}
};
// Clean up on unmount
React.useEffect(() => {
return () => {
clearDelay();
};
}, []);
return delayInvoke;
}
@@ -0,0 +1,3 @@
import type * as React from 'react';
import type { AlignType } from '../interface';
export default function useOffsetStyle(isMobile: boolean, ready: boolean, open: boolean, align: AlignType, offsetR: number, offsetB: number, offsetX: number, offsetY: number): React.CSSProperties;
@@ -0,0 +1,41 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useOffsetStyle;
function useOffsetStyle(isMobile, ready, open, align, offsetR, offsetB, offsetX, offsetY) {
// >>>>> Offset
const AUTO = 'auto';
const offsetStyle = isMobile ? {} : {
left: '-1000vw',
top: '-1000vh',
right: AUTO,
bottom: AUTO
};
// Set align style
if (!isMobile && (ready || !open)) {
const {
points
} = align;
const dynamicInset = align.dynamicInset || align._experimental?.dynamicInset;
const alignRight = dynamicInset && points[0][1] === 'r';
const alignBottom = dynamicInset && points[0][0] === 'b';
if (alignRight) {
offsetStyle.right = offsetR;
offsetStyle.left = AUTO;
} else {
offsetStyle.left = offsetX;
offsetStyle.right = AUTO;
}
if (alignBottom) {
offsetStyle.bottom = offsetB;
offsetStyle.top = AUTO;
} else {
offsetStyle.top = offsetY;
offsetStyle.bottom = AUTO;
}
}
return offsetStyle;
}
@@ -0,0 +1 @@
export default function useWatch(open: boolean, target: HTMLElement, popup: HTMLElement, onAlign: VoidFunction, onScroll: VoidFunction): void;
@@ -0,0 +1,42 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useWatch;
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var _util = require("../util");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function useWatch(open, target, popup, onAlign, onScroll) {
(0, _useLayoutEffect.default)(() => {
if (open && target && popup) {
const targetElement = target;
const popupElement = popup;
const targetScrollList = (0, _util.collectScroller)(targetElement);
const popupScrollList = (0, _util.collectScroller)(popupElement);
const win = (0, _util.getWin)(popupElement);
const mergedList = new Set([win, ...targetScrollList, ...popupScrollList]);
function notifyScroll() {
onAlign();
onScroll();
}
mergedList.forEach(scroller => {
scroller.addEventListener('scroll', notifyScroll, {
passive: true
});
});
win.addEventListener('resize', notifyScroll, {
passive: true
});
// First time always do align
onAlign();
return () => {
mergedList.forEach(scroller => {
scroller.removeEventListener('scroll', notifyScroll);
win.removeEventListener('resize', notifyScroll);
});
};
}
}, [open, target, popup]);
}
@@ -0,0 +1,5 @@
/**
* Close if click on the window.
* Return the function that click on the Popup element.
*/
export default function useWinClick(open: boolean, clickToHide: boolean, targetEle: HTMLElement, popupEle: HTMLElement, mask: boolean, maskClosable: boolean, inPopupOrChild: (target: EventTarget) => boolean, triggerOpen: (open: boolean) => void): () => void;
@@ -0,0 +1,66 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = useWinClick;
var _shadow = require("@rc-component/util/lib/Dom/shadow");
var _warning = require("@rc-component/util/lib/warning");
var React = _interopRequireWildcard(require("react"));
var _util = require("../util");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
/**
* Close if click on the window.
* Return the function that click on the Popup element.
*/
function useWinClick(open, clickToHide, targetEle, popupEle, mask, maskClosable, inPopupOrChild, triggerOpen) {
const openRef = React.useRef(open);
openRef.current = open;
const popupPointerDownRef = React.useRef(false);
// Click to hide is special action since click popup element should not hide
React.useEffect(() => {
if (clickToHide && popupEle && (!mask || maskClosable)) {
const onPointerDown = () => {
popupPointerDownRef.current = false;
};
const onTriggerClose = e => {
if (openRef.current && !inPopupOrChild(e.composedPath?.()?.[0] || e.target) && !popupPointerDownRef.current) {
triggerOpen(false);
}
};
const win = (0, _util.getWin)(popupEle);
win.addEventListener('pointerdown', onPointerDown, true);
win.addEventListener('mousedown', onTriggerClose, true);
win.addEventListener('contextmenu', onTriggerClose, true);
// shadow root
const targetShadowRoot = (0, _shadow.getShadowRoot)(targetEle);
if (targetShadowRoot) {
targetShadowRoot.addEventListener('mousedown', onTriggerClose, true);
targetShadowRoot.addEventListener('contextmenu', onTriggerClose, true);
}
// Warning if target and popup not in same root
if (process.env.NODE_ENV !== 'production' && targetEle) {
const targetRoot = targetEle.getRootNode?.();
const popupRoot = popupEle.getRootNode?.();
(0, _warning.warning)(targetRoot === popupRoot, `trigger element and popup element should in same shadow root.`);
}
return () => {
win.removeEventListener('pointerdown', onPointerDown, true);
win.removeEventListener('mousedown', onTriggerClose, true);
win.removeEventListener('contextmenu', onTriggerClose, true);
if (targetShadowRoot) {
targetShadowRoot.removeEventListener('mousedown', onTriggerClose, true);
targetShadowRoot.removeEventListener('contextmenu', onTriggerClose, true);
}
};
}
}, [clickToHide, targetEle, popupEle, mask, maskClosable]);
function onPopupPointerDown() {
popupPointerDownRef.current = true;
}
return onPopupPointerDown;
}
+80
View File
@@ -0,0 +1,80 @@
import type { CSSMotionProps } from '@rc-component/motion';
import * as React from 'react';
import { type MobileConfig } from './Popup';
import type { ActionType, AlignType, ArrowTypeOuter, BuildInPlacements } from './interface';
export type { ActionType, AlignType, ArrowTypeOuter as ArrowType, BuildInPlacements, };
import UniqueProvider, { type UniqueProviderProps } from './UniqueProvider';
export { UniqueProvider };
export type { UniqueProviderProps };
export interface TriggerRef {
nativeElement: HTMLElement;
popupElement: HTMLDivElement;
forceAlign: VoidFunction;
}
export interface TriggerProps {
children: React.ReactElement<any> | ((info: {
open: boolean;
}) => React.ReactElement<any>);
action?: ActionType | ActionType[];
showAction?: ActionType[];
hideAction?: ActionType[];
prefixCls?: string;
zIndex?: number;
onPopupAlign?: (element: HTMLElement, align: AlignType) => void;
stretch?: string;
popupVisible?: boolean;
defaultPopupVisible?: boolean;
onOpenChange?: (visible: boolean) => void;
afterOpenChange?: (visible: boolean) => void;
/** @deprecated Use `onOpenChange` instead */
onPopupVisibleChange?: (visible: boolean) => void;
/** @deprecated Use `afterOpenChange` instead */
afterPopupVisibleChange?: (visible: boolean) => void;
getPopupContainer?: (node: HTMLElement) => HTMLElement;
forceRender?: boolean;
autoDestroy?: boolean;
mask?: boolean;
maskClosable?: boolean;
/** Set popup motion. You can ref `rc-motion` for more info. */
popupMotion?: CSSMotionProps;
/** Set mask motion. You can ref `rc-motion` for more info. */
maskMotion?: CSSMotionProps;
mouseEnterDelay?: number;
mouseLeaveDelay?: number;
focusDelay?: number;
blurDelay?: number;
popup: React.ReactNode | (() => React.ReactNode);
popupPlacement?: string;
builtinPlacements?: BuildInPlacements;
popupAlign?: AlignType;
popupClassName?: string;
/** Pass to `UniqueProvider` UniqueContainer */
uniqueContainerClassName?: string;
/** Pass to `UniqueProvider` UniqueContainer */
uniqueContainerStyle?: React.CSSProperties;
popupStyle?: React.CSSProperties;
getPopupClassNameFromAlign?: (align: AlignType) => string;
onPopupClick?: React.MouseEventHandler<HTMLDivElement>;
alignPoint?: boolean;
/**
* Trigger will memo content when close.
* This may affect the case if want to keep content update.
* Set `fresh` to `false` will always keep update.
*/
fresh?: boolean;
/**
* Config with UniqueProvider to shared the floating popup.
*/
unique?: boolean;
arrow?: boolean | ArrowTypeOuter;
/**
* @private Bump fixed position at bottom in mobile.
* Will replace the config of root props.
* This will directly trade as mobile view which will not check what real is.
* This is internal usage currently, do not use in your prod.
*/
mobile?: MobileConfig;
}
export declare function generateTrigger(PortalComponent?: React.ComponentType<any>): React.ForwardRefExoticComponent<TriggerProps & React.RefAttributes<TriggerRef>>;
declare const _default: React.ForwardRefExoticComponent<TriggerProps & React.RefAttributes<TriggerRef>>;
export default _default;
+576
View File
@@ -0,0 +1,576 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "UniqueProvider", {
enumerable: true,
get: function () {
return _UniqueProvider.default;
}
});
exports.default = void 0;
exports.generateTrigger = generateTrigger;
var _portal = _interopRequireDefault(require("@rc-component/portal"));
var _clsx = require("clsx");
var _resizeObserver = require("@rc-component/resize-observer");
var _findDOMNode = require("@rc-component/util/lib/Dom/findDOMNode");
var _shadow = require("@rc-component/util/lib/Dom/shadow");
var _ref = require("@rc-component/util/lib/ref");
var _useEvent = _interopRequireDefault(require("@rc-component/util/lib/hooks/useEvent"));
var _useId = _interopRequireDefault(require("@rc-component/util/lib/hooks/useId"));
var _useLayoutEffect = _interopRequireDefault(require("@rc-component/util/lib/hooks/useLayoutEffect"));
var React = _interopRequireWildcard(require("react"));
var _Popup = _interopRequireDefault(require("./Popup"));
var _context = _interopRequireWildcard(require("./context"));
var _useAction = _interopRequireDefault(require("./hooks/useAction"));
var _useAlign = _interopRequireDefault(require("./hooks/useAlign"));
var _useDelay = _interopRequireDefault(require("./hooks/useDelay"));
var _useWatch = _interopRequireDefault(require("./hooks/useWatch"));
var _useWinClick = _interopRequireDefault(require("./hooks/useWinClick"));
var _util = require("./util");
var _UniqueProvider = _interopRequireDefault(require("./UniqueProvider"));
var _util2 = require("@rc-component/util");
var _reactDom = require("react-dom");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
// Removed Props List
// Seems this can be auto
// getDocument?: (element?: HTMLElement) => Document;
// New version will not wrap popup with `rc-trigger-popup-content` when multiple children
function generateTrigger(PortalComponent = _portal.default) {
const Trigger = /*#__PURE__*/React.forwardRef((props, ref) => {
const {
prefixCls = 'rc-trigger-popup',
children,
// Action
action = 'hover',
showAction,
hideAction,
// Open
popupVisible,
defaultPopupVisible,
onOpenChange,
afterOpenChange,
onPopupVisibleChange,
afterPopupVisibleChange,
// Delay
mouseEnterDelay,
mouseLeaveDelay = 0.1,
focusDelay,
blurDelay,
// Mask
mask,
maskClosable = true,
// Portal
getPopupContainer,
forceRender,
autoDestroy,
// Popup
popup,
popupClassName,
uniqueContainerClassName,
uniqueContainerStyle,
popupStyle,
popupPlacement,
builtinPlacements = {},
popupAlign,
zIndex,
stretch,
getPopupClassNameFromAlign,
fresh,
unique,
alignPoint,
onPopupClick,
onPopupAlign,
// Arrow
arrow,
// Motion
popupMotion,
maskMotion,
// Private
mobile,
...restProps
} = props;
const mergedAutoDestroy = autoDestroy || false;
const openUncontrolled = popupVisible === undefined;
// =========================== Mobile ===========================
const isMobile = !!mobile;
// ========================== Context ===========================
const subPopupElements = React.useRef({});
const parentContext = React.useContext(_context.default);
const context = React.useMemo(() => {
return {
registerSubPopup: (id, subPopupEle) => {
subPopupElements.current[id] = subPopupEle;
parentContext?.registerSubPopup(id, subPopupEle);
}
};
}, [parentContext]);
// ======================== UniqueContext =========================
const uniqueContext = React.useContext(_context.UniqueContext);
// =========================== Popup ============================
const id = (0, _useId.default)();
const [popupEle, setPopupEle] = React.useState(null);
// Used for forwardRef popup. Not use internal
const externalPopupRef = React.useRef(null);
const setPopupRef = (0, _useEvent.default)(node => {
externalPopupRef.current = node;
if ((0, _findDOMNode.isDOM)(node) && popupEle !== node) {
setPopupEle(node);
}
parentContext?.registerSubPopup(id, node);
});
// =========================== Target ===========================
// Use state to control here since `useRef` update not trigger render
const [targetEle, setTargetEle] = React.useState(null);
// Used for forwardRef target. Not use internal
const externalForwardRef = React.useRef(null);
const setTargetRef = (0, _useEvent.default)(node => {
const domNode = (0, _findDOMNode.getDOM)(node);
if ((0, _findDOMNode.isDOM)(domNode) && targetEle !== domNode) {
setTargetEle(domNode);
externalForwardRef.current = domNode;
}
});
const cloneProps = {};
const inPopupOrChild = (0, _useEvent.default)(ele => {
const childDOM = targetEle;
return childDOM?.contains(ele) || (0, _shadow.getShadowRoot)(childDOM)?.host === ele || ele === childDOM || popupEle?.contains(ele) || (0, _shadow.getShadowRoot)(popupEle)?.host === ele || ele === popupEle || Object.values(subPopupElements.current).some(subPopupEle => subPopupEle?.contains(ele) || ele === subPopupEle);
});
// =========================== Arrow ============================
const innerArrow = arrow ? {
// true and Object likely
...(arrow !== true ? arrow : {})
} : null;
// ============================ Open ============================
const [internalOpen, setInternalOpen] = (0, _util2.useControlledState)(defaultPopupVisible || false, popupVisible);
const mergedOpen = internalOpen || false;
// ========================== Children ==========================
const child = React.useMemo(() => {
const nextChild = typeof children === 'function' ? children({
open: mergedOpen
}) : children;
return React.Children.only(nextChild);
}, [children, mergedOpen]);
const originChildProps = child?.props || {};
// Support ref
const isOpen = (0, _useEvent.default)(() => mergedOpen);
// Extract common options for UniqueProvider
const getUniqueOptions = (0, _useEvent.default)((delay = 0) => ({
popup,
target: targetEle,
delay,
prefixCls,
popupClassName,
uniqueContainerClassName,
uniqueContainerStyle,
popupStyle,
popupPlacement,
builtinPlacements,
popupAlign,
zIndex,
mask,
maskClosable,
popupMotion,
maskMotion,
arrow: innerArrow,
getPopupContainer,
getPopupClassNameFromAlign,
id,
onEsc
}));
// Handle controlled state changes for UniqueProvider
// Only sync to UniqueProvider when it's controlled mode
// If there is a parentContext, don't call uniqueContext methods
(0, _useLayoutEffect.default)(() => {
if (uniqueContext && unique && targetEle && !openUncontrolled && !parentContext) {
if (mergedOpen) {
uniqueContext.show(getUniqueOptions(mouseEnterDelay), isOpen);
} else {
uniqueContext.hide(mouseLeaveDelay);
}
}
}, [mergedOpen, targetEle]);
const openRef = React.useRef(mergedOpen);
openRef.current = mergedOpen;
const internalTriggerOpen = (0, _useEvent.default)(nextOpen => {
(0, _reactDom.flushSync)(() => {
if (mergedOpen !== nextOpen) {
setInternalOpen(nextOpen);
onOpenChange?.(nextOpen);
onPopupVisibleChange?.(nextOpen);
}
});
});
// Trigger for delay
const delayInvoke = (0, _useDelay.default)();
const triggerOpen = (nextOpen, delay = 0) => {
// If it's controlled mode, always use internal trigger logic
// UniqueProvider will be synced through useLayoutEffect
if (popupVisible !== undefined) {
delayInvoke(() => {
internalTriggerOpen(nextOpen);
}, delay);
return;
}
// If UniqueContext exists and not controlled, pass delay to Provider instead of handling it internally
// If there is a parentContext, don't call uniqueContext methods
if (uniqueContext && unique && openUncontrolled && !parentContext) {
if (nextOpen) {
uniqueContext.show(getUniqueOptions(delay), isOpen);
} else {
uniqueContext.hide(delay);
}
return;
}
delayInvoke(() => {
internalTriggerOpen(nextOpen);
}, delay);
};
function onEsc({
top
}) {
if (top) {
triggerOpen(false);
}
}
// ========================== Motion ============================
const [inMotion, setInMotion] = React.useState(false);
(0, _useLayoutEffect.default)(firstMount => {
if (!firstMount || mergedOpen) {
setInMotion(true);
}
}, [mergedOpen]);
const [motionPrepareResolve, setMotionPrepareResolve] = React.useState(null);
// =========================== Align ============================
const [mousePos, setMousePos] = React.useState(null);
const setMousePosByEvent = event => {
setMousePos([event.clientX, event.clientY]);
};
const [ready, offsetX, offsetY, offsetR, offsetB, arrowX, arrowY, scaleX, scaleY, alignInfo, onAlign] = (0, _useAlign.default)(mergedOpen, popupEle, alignPoint && mousePos !== null ? mousePos : targetEle, popupPlacement, builtinPlacements, popupAlign, onPopupAlign, isMobile);
const [showActions, hideActions] = (0, _useAction.default)(action, showAction, hideAction);
const clickToShow = showActions.has('click');
const clickToHide = hideActions.has('click') || hideActions.has('contextMenu');
const triggerAlign = (0, _useEvent.default)(() => {
if (!inMotion) {
onAlign();
}
});
const onScroll = () => {
if (openRef.current && alignPoint && clickToHide) {
triggerOpen(false);
}
};
(0, _useWatch.default)(mergedOpen, targetEle, popupEle, triggerAlign, onScroll);
(0, _useLayoutEffect.default)(() => {
triggerAlign();
}, [mousePos, popupPlacement]);
// When no builtinPlacements and popupAlign changed
(0, _useLayoutEffect.default)(() => {
if (mergedOpen && !builtinPlacements?.[popupPlacement]) {
triggerAlign();
}
}, [JSON.stringify(popupAlign)]);
const alignedClassName = React.useMemo(() => {
const baseClassName = (0, _util.getAlignPopupClassName)(builtinPlacements, prefixCls, alignInfo, alignPoint);
return (0, _clsx.clsx)(baseClassName, getPopupClassNameFromAlign?.(alignInfo));
}, [alignInfo, getPopupClassNameFromAlign, builtinPlacements, prefixCls, alignPoint]);
// ============================ Refs ============================
React.useImperativeHandle(ref, () => ({
nativeElement: externalForwardRef.current,
popupElement: externalPopupRef.current,
forceAlign: triggerAlign
}));
// ========================== Stretch ===========================
const [targetWidth, setTargetWidth] = React.useState(0);
const [targetHeight, setTargetHeight] = React.useState(0);
const syncTargetSize = () => {
if (stretch && targetEle) {
const rect = targetEle.getBoundingClientRect();
setTargetWidth(rect.width);
setTargetHeight(rect.height);
}
};
const onTargetResize = () => {
syncTargetSize();
triggerAlign();
};
// ========================== Motion ============================
const onVisibleChanged = visible => {
setInMotion(false);
onAlign();
afterOpenChange?.(visible);
afterPopupVisibleChange?.(visible);
};
// We will trigger align when motion is in prepare
const onPrepare = () => new Promise(resolve => {
syncTargetSize();
setMotionPrepareResolve(() => resolve);
});
(0, _useLayoutEffect.default)(() => {
if (motionPrepareResolve) {
onAlign();
motionPrepareResolve();
setMotionPrepareResolve(null);
}
}, [motionPrepareResolve]);
// =========================== Action ===========================
/**
* Util wrapper for trigger action
* @param eventName Listen event name
* @param nextOpen Next open state after trigger
* @param delay Delay to trigger open change
* @param callback Callback if current event need additional action
* @param ignoreCheck Ignore current event if check return true
*/
function wrapperAction(eventName, nextOpen, delay, callback, ignoreCheck) {
cloneProps[eventName] = (event, ...args) => {
if (!ignoreCheck || !ignoreCheck()) {
callback?.(event);
triggerOpen(nextOpen, delay);
}
// Pass to origin
originChildProps[eventName]?.(event, ...args);
};
}
// ======================= Action: Touch ========================
const touchToShow = showActions.has('touch');
const touchToHide = hideActions.has('touch');
/** Used for prevent `hover` event conflict with mobile env */
const touchedRef = React.useRef(false);
if (touchToShow || touchToHide) {
cloneProps.onTouchStart = (...args) => {
touchedRef.current = true;
if (openRef.current && touchToHide) {
triggerOpen(false);
} else if (!openRef.current && touchToShow) {
triggerOpen(true);
}
// Pass to origin
originChildProps.onTouchStart?.(...args);
};
}
// ======================= Action: Click ========================
if (clickToShow || clickToHide) {
cloneProps.onClick = (event, ...args) => {
if (openRef.current && clickToHide) {
triggerOpen(false);
} else if (!openRef.current && clickToShow) {
setMousePosByEvent(event);
triggerOpen(true);
}
// Pass to origin
originChildProps.onClick?.(event, ...args);
touchedRef.current = false;
};
}
// Click to hide is special action since click popup element should not hide
const onPopupPointerDown = (0, _useWinClick.default)(mergedOpen, clickToHide || touchToHide, targetEle, popupEle, mask, maskClosable, inPopupOrChild, triggerOpen);
// ======================= Action: Hover ========================
const hoverToShow = showActions.has('hover');
const hoverToHide = hideActions.has('hover');
let onPopupMouseEnter;
let onPopupMouseLeave;
const ignoreMouseTrigger = () => {
return touchedRef.current;
};
if (hoverToShow) {
const onMouseEnterCallback = event => {
setMousePosByEvent(event);
};
// Compatible with old browser which not support pointer event
wrapperAction('onMouseEnter', true, mouseEnterDelay, onMouseEnterCallback, ignoreMouseTrigger);
wrapperAction('onPointerEnter', true, mouseEnterDelay, onMouseEnterCallback, ignoreMouseTrigger);
onPopupMouseEnter = event => {
// Only trigger re-open when popup is visible
if ((mergedOpen || inMotion) && popupEle?.contains(event.target)) {
triggerOpen(true, mouseEnterDelay);
}
};
// Align Point
if (alignPoint) {
cloneProps.onMouseMove = event => {
originChildProps.onMouseMove?.(event);
};
}
}
if (hoverToHide) {
wrapperAction('onMouseLeave', false, mouseLeaveDelay, undefined, ignoreMouseTrigger);
wrapperAction('onPointerLeave', false, mouseLeaveDelay, undefined, ignoreMouseTrigger);
onPopupMouseLeave = () => {
triggerOpen(false, mouseLeaveDelay);
};
}
// ======================= Action: Focus ========================
if (showActions.has('focus')) {
wrapperAction('onFocus', true, focusDelay);
}
if (hideActions.has('focus')) {
wrapperAction('onBlur', false, blurDelay);
}
// ==================== Action: ContextMenu =====================
if (showActions.has('contextMenu')) {
cloneProps.onContextMenu = (event, ...args) => {
if (openRef.current && hideActions.has('contextMenu')) {
triggerOpen(false);
} else {
setMousePosByEvent(event);
triggerOpen(true);
}
event.preventDefault();
// Pass to origin
originChildProps.onContextMenu?.(event, ...args);
};
}
// ============================ Perf ============================
const rendedRef = React.useRef(false);
rendedRef.current ||= forceRender || mergedOpen || inMotion;
// =========================== Render ===========================
const mergedChildrenProps = {
...originChildProps,
...cloneProps
};
// Pass props into cloneProps for nest usage
const passedProps = {};
const passedEventList = ['onContextMenu', 'onClick', 'onMouseDown', 'onTouchStart', 'onMouseEnter', 'onMouseLeave', 'onFocus', 'onBlur'];
passedEventList.forEach(eventName => {
if (restProps[eventName]) {
passedProps[eventName] = (...args) => {
mergedChildrenProps[eventName]?.(...args);
restProps[eventName](...args);
};
}
});
const arrowPos = {
x: arrowX,
y: arrowY
};
// =================== Resize Observer ===================
// Use hook to observe target element resize
// Pass targetEle directly instead of a function so the hook will re-observe when target changes
(0, _resizeObserver.useResizeObserver)(mergedOpen, targetEle, onTargetResize);
// Compose refs
const mergedRef = (0, _ref.useComposeRef)(setTargetRef, (0, _ref.getNodeRef)(child));
// Child Node
const triggerNode = /*#__PURE__*/React.cloneElement(child, {
...mergedChildrenProps,
...passedProps,
ref: mergedRef
});
// Render
return /*#__PURE__*/React.createElement(React.Fragment, null, triggerNode, rendedRef.current && (!uniqueContext || !unique) && /*#__PURE__*/React.createElement(_context.default.Provider, {
value: context
}, /*#__PURE__*/React.createElement(_Popup.default, {
portal: PortalComponent,
ref: setPopupRef,
prefixCls: prefixCls,
popup: popup,
className: (0, _clsx.clsx)(popupClassName, !isMobile && alignedClassName),
style: popupStyle,
target: targetEle,
onMouseEnter: onPopupMouseEnter,
onMouseLeave: onPopupMouseLeave
// https://github.com/ant-design/ant-design/issues/43924
,
onPointerEnter: onPopupMouseEnter,
zIndex: zIndex
// Open
,
open: mergedOpen,
keepDom: inMotion,
fresh: fresh
// Click
,
onClick: onPopupClick,
onPointerDownCapture: onPopupPointerDown
// Mask
,
mask: mask
// Motion
,
motion: popupMotion,
maskMotion: maskMotion,
onVisibleChanged: onVisibleChanged,
onPrepare: onPrepare
// Portal
,
forceRender: forceRender,
autoDestroy: mergedAutoDestroy,
getPopupContainer: getPopupContainer,
onEsc: onEsc
// Arrow
,
align: alignInfo,
arrow: innerArrow,
arrowPos: arrowPos
// Align
,
ready: ready,
offsetX: offsetX,
offsetY: offsetY,
offsetR: offsetR,
offsetB: offsetB,
onAlign: triggerAlign
// Stretch
,
stretch: stretch,
targetWidth: targetWidth / scaleX,
targetHeight: targetHeight / scaleY
// Mobile
,
mobile: mobile
})));
});
if (process.env.NODE_ENV !== 'production') {
Trigger.displayName = 'Trigger';
}
return Trigger;
}
var _default = exports.default = generateTrigger(_portal.default);
+92
View File
@@ -0,0 +1,92 @@
/// <reference types="react" />
export type Placement = 'top' | 'left' | 'right' | 'bottom' | 'topLeft' | 'topRight' | 'bottomLeft' | 'bottomRight' | 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom';
export type AlignPointTopBottom = 't' | 'b' | 'c';
export type AlignPointLeftRight = 'l' | 'r' | 'c';
/** Two char of 't' 'b' 'c' 'l' 'r'. Example: 'lt' */
export type AlignPoint = `${AlignPointTopBottom}${AlignPointLeftRight}`;
export type OffsetType = number | `${number}%`;
export interface AlignType {
/**
* move point of source node to align with point of target node.
* Such as ['tr','cc'], align top right point of source node with center point of target node.
* Point can be 't'(top), 'b'(bottom), 'c'(center), 'l'(left), 'r'(right) */
points?: (string | AlignPoint)[];
/**
* @private Do not use in your production code
*/
_experimental?: Record<string, any>;
/**
* offset source node by offset[0] in x and offset[1] in y.
* If offset contains percentage string value, it is relative to sourceNode region.
*/
offset?: OffsetType[];
/**
* offset target node by offset[0] in x and offset[1] in y.
* If targetOffset contains percentage string value, it is relative to targetNode region.
*/
targetOffset?: OffsetType[];
/**
* If adjustX field is true, will adjust source node in x direction if source node is invisible.
* If adjustY field is true, will adjust source node in y direction if source node is invisible.
*/
overflow?: {
adjustX?: boolean | number;
adjustY?: boolean | number;
shiftX?: boolean | number;
shiftY?: boolean | number;
};
/** Auto adjust arrow position */
autoArrow?: boolean;
/**
* Config visible region check of html node. Default `visible`:
* - `visible`:
* The visible region of user browser window.
* Use `clientHeight` for check.
* If `visible` region not satisfy, fallback to `scroll`.
* - `scroll`:
* The whole region of the html scroll area.
* Use `scrollHeight` for check.
* - `visibleFirst`:
* Similar to `visible`, but if `visible` region not satisfy, fallback to `scroll`.
*/
htmlRegion?: 'visible' | 'scroll' | 'visibleFirst';
/**
* Auto chose position with `top` or `bottom` by the align result
*/
dynamicInset?: boolean;
/**
* Whether use css right instead of left to position
*/
useCssRight?: boolean;
/**
* Whether use css bottom instead of top to position
*/
useCssBottom?: boolean;
/**
* Whether use css transform instead of left/top/right/bottom to position if browser supports.
* Defaults to false.
*/
useCssTransform?: boolean;
ignoreShake?: boolean;
}
export interface ArrowTypeOuter {
style?: React.CSSProperties;
className?: string;
content?: React.ReactNode;
}
export type ArrowPos = {
x?: number;
y?: number;
};
export type BuildInPlacements = Record<string, AlignType>;
export type StretchType = string;
export type ActionType = 'hover' | 'focus' | 'click' | 'contextMenu';
export type AnimationType = string;
export type TransitionNameType = string;
export interface Point {
pageX: number;
pageY: number;
}
export interface CommonEventHandler {
remove: () => void;
}
+5
View File
@@ -0,0 +1,5 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
+5
View File
@@ -0,0 +1,5 @@
import * as React from 'react';
import { UniqueProvider } from './index';
declare const _default: React.ForwardRefExoticComponent<import("./index").TriggerProps & React.RefAttributes<import("./index").TriggerRef>>;
export default _default;
export { UniqueProvider };
+36
View File
@@ -0,0 +1,36 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
Object.defineProperty(exports, "UniqueProvider", {
enumerable: true,
get: function () {
return _index.UniqueProvider;
}
});
exports.default = void 0;
var React = _interopRequireWildcard(require("react"));
var _index = require("./index");
function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); }
function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; }
const MockPortal = ({
open,
autoDestroy,
children,
getContainer
}) => {
const [visible, setVisible] = React.useState(open);
React.useEffect(() => {
getContainer?.();
});
React.useEffect(() => {
if (open) {
setVisible(true);
} else if (autoDestroy) {
setVisible(false);
}
}, [open, autoDestroy]);
return visible ? children : null;
};
var _default = exports.default = (0, _index.generateTrigger)(MockPortal);
+44
View File
@@ -0,0 +1,44 @@
import type { AlignType, BuildInPlacements } from './interface';
export declare function getAlignPopupClassName(builtinPlacements: BuildInPlacements, prefixCls: string, align: AlignType, isAlignPoint: boolean): string;
export declare function getWin(ele: HTMLElement): Window & typeof globalThis;
/**
* Get all the scrollable parent elements of the element
* @param ele The element to be detected
* @param areaOnly Only return the parent which will cut visible area
*/
export declare function collectScroller(ele: HTMLElement): HTMLElement[];
export declare function toNum(num: number, defaultValue?: number): number;
export interface VisibleArea {
left: number;
top: number;
right: number;
bottom: number;
}
/**
*
*
* **************************************
* * Border *
* * ************************** *
* * * * * *
* * B * * S * B *
* * o * * c * o *
* * r * Content * r * r *
* * d * * o * d *
* * e * * l * e *
* * r ******************** l * r *
* * * Scroll * *
* * ************************** *
* * Border *
* **************************************
*
*/
/**
* Get visible area of element
*/
export declare function getVisibleArea(initArea: VisibleArea, scrollerList?: HTMLElement[]): {
left: number;
top: number;
right: number;
bottom: number;
};
+147
View File
@@ -0,0 +1,147 @@
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.collectScroller = collectScroller;
exports.getAlignPopupClassName = getAlignPopupClassName;
exports.getVisibleArea = getVisibleArea;
exports.getWin = getWin;
exports.toNum = toNum;
function isPointsEq(a1 = [], a2 = [], isAlignPoint) {
const getVal = (a, index) => a[index] || '';
if (isAlignPoint) {
return getVal(a1, 0) === getVal(a2, 0);
}
return getVal(a1, 0) === getVal(a2, 0) && getVal(a1, 1) === getVal(a2, 1);
}
function getAlignPopupClassName(builtinPlacements, prefixCls, align, isAlignPoint) {
const {
points
} = align;
const placements = Object.keys(builtinPlacements);
for (let i = 0; i < placements.length; i += 1) {
const placement = placements[i];
if (isPointsEq(builtinPlacements[placement]?.points, points, isAlignPoint)) {
return `${prefixCls}-placement-${placement}`;
}
}
return '';
}
function getWin(ele) {
return ele.ownerDocument.defaultView;
}
/**
* Get all the scrollable parent elements of the element
* @param ele The element to be detected
* @param areaOnly Only return the parent which will cut visible area
*/
function collectScroller(ele) {
const scrollerList = [];
let current = ele?.parentElement;
const scrollStyle = ['hidden', 'scroll', 'clip', 'auto'];
while (current) {
const {
overflowX,
overflowY,
overflow
} = getWin(current).getComputedStyle(current);
if ([overflowX, overflowY, overflow].some(o => scrollStyle.includes(o))) {
scrollerList.push(current);
}
current = current.parentElement;
}
return scrollerList;
}
function toNum(num, defaultValue = 1) {
return Number.isNaN(num) ? defaultValue : num;
}
function getPxValue(val) {
return toNum(parseFloat(val), 0);
}
/**
*
*
* **************************************
* * Border *
* * ************************** *
* * * * * *
* * B * * S * B *
* * o * * c * o *
* * r * Content * r * r *
* * d * * o * d *
* * e * * l * e *
* * r ******************** l * r *
* * * Scroll * *
* * ************************** *
* * Border *
* **************************************
*
*/
/**
* Get visible area of element
*/
function getVisibleArea(initArea, scrollerList) {
const visibleArea = {
...initArea
};
(scrollerList || []).forEach(ele => {
if (ele instanceof HTMLBodyElement || ele instanceof HTMLHtmlElement) {
return;
}
// Skip if static position which will not affect visible area
const {
overflow,
overflowClipMargin,
borderTopWidth,
borderBottomWidth,
borderLeftWidth,
borderRightWidth
} = getWin(ele).getComputedStyle(ele);
const eleRect = ele.getBoundingClientRect();
const {
offsetHeight: eleOutHeight,
clientHeight: eleInnerHeight,
offsetWidth: eleOutWidth,
clientWidth: eleInnerWidth
} = ele;
const borderTopNum = getPxValue(borderTopWidth);
const borderBottomNum = getPxValue(borderBottomWidth);
const borderLeftNum = getPxValue(borderLeftWidth);
const borderRightNum = getPxValue(borderRightWidth);
const scaleX = toNum(Math.round(eleRect.width / eleOutWidth * 1000) / 1000);
const scaleY = toNum(Math.round(eleRect.height / eleOutHeight * 1000) / 1000);
// Original visible area
const eleScrollWidth = (eleOutWidth - eleInnerWidth - borderLeftNum - borderRightNum) * scaleX;
const eleScrollHeight = (eleOutHeight - eleInnerHeight - borderTopNum - borderBottomNum) * scaleY;
// Cut border size
const scaledBorderTopWidth = borderTopNum * scaleY;
const scaledBorderBottomWidth = borderBottomNum * scaleY;
const scaledBorderLeftWidth = borderLeftNum * scaleX;
const scaledBorderRightWidth = borderRightNum * scaleX;
// Clip margin
let clipMarginWidth = 0;
let clipMarginHeight = 0;
if (overflow === 'clip') {
const clipNum = getPxValue(overflowClipMargin);
clipMarginWidth = clipNum * scaleX;
clipMarginHeight = clipNum * scaleY;
}
// Region
const eleLeft = eleRect.x + scaledBorderLeftWidth - clipMarginWidth;
const eleTop = eleRect.y + scaledBorderTopWidth - clipMarginHeight;
const eleRight = eleLeft + eleRect.width + 2 * clipMarginWidth - scaledBorderLeftWidth - scaledBorderRightWidth - eleScrollWidth;
const eleBottom = eleTop + eleRect.height + 2 * clipMarginHeight - scaledBorderTopWidth - scaledBorderBottomWidth - eleScrollHeight;
visibleArea.left = Math.max(visibleArea.left, eleLeft);
visibleArea.top = Math.max(visibleArea.top, eleTop);
visibleArea.right = Math.min(visibleArea.right, eleRight);
visibleArea.bottom = Math.min(visibleArea.bottom, eleBottom);
});
return visibleArea;
}
+76
View File
@@ -0,0 +1,76 @@
{
"name": "@rc-component/trigger",
"version": "3.9.0",
"description": "base abstract trigger component for react",
"engines": {
"node": ">=8.x"
},
"keywords": [
"react",
"react-component",
"react-trigger",
"trigger"
],
"homepage": "https://github.com/react-component/trigger",
"author": "",
"repository": {
"type": "git",
"url": "https://github.com/react-component/trigger.git"
},
"bugs": {
"url": "https://github.com/react-component/trigger/issues"
},
"files": [
"es",
"lib",
"assets/**/*.css",
"assets/**/*.less"
],
"license": "MIT",
"main": "./lib/index",
"module": "./es/index",
"scripts": {
"start": "dumi dev",
"build": "dumi build",
"compile": "father build && lessc assets/index.less assets/index.css",
"prepublishOnly": "npm run compile && rc-np",
"lint": "eslint src/ docs/examples/ --ext .tsx,.ts,.jsx,.js",
"test": "rc-test",
"prettier": "prettier --write .",
"coverage": "rc-test --coverage",
"now-build": "npm run build"
},
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/portal": "^2.2.0",
"@rc-component/resize-observer": "^1.1.1",
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"devDependencies": {
"@rc-component/father-plugin": "^2.0.0",
"@rc-component/np": "^1.0.3",
"@testing-library/jest-dom": "^6.1.4",
"@testing-library/react": "^16.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^25.0.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@umijs/fabric": "^4.0.1",
"cross-env": "^10.1.0",
"dumi": "^2.1.0",
"eslint": "^8.51.0",
"father": "^4.0.0",
"less": "^4.2.0",
"prettier": "^3.3.3",
"rc-test": "^7.0.13",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"regenerator-runtime": "^0.14.0",
"typescript": "~5.9.3"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
}