1
This commit is contained in:
+5
@@ -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 {};
|
||||
+19
@@ -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]);
|
||||
}
|
||||
+15
@@ -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
@@ -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];
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function useDelay(): (callback: VoidFunction, delay: number) => void;
|
||||
+28
@@ -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;
|
||||
}
|
||||
+3
@@ -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;
|
||||
+35
@@ -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;
|
||||
}
|
||||
+1
@@ -0,0 +1 @@
|
||||
export default function useWatch(open: boolean, target: HTMLElement, popup: HTMLElement, onAlign: VoidFunction, onScroll: VoidFunction): void;
|
||||
+35
@@ -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]);
|
||||
}
|
||||
+5
@@ -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;
|
||||
+59
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user