0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00
logto/packages/console/src/hooks/use-position.ts
renovate[bot] 8508469abf
chore(deps): update silverhand configs monorepo packages to v6 (major) (#5750)
* chore: upgrade configs

* refactor: fix lint errors

* refactor: fix lint errors

* refactor: fix stylelint issues

---------

Co-authored-by: Gao Sun <gao@silverhand.io>
2024-04-18 12:26:37 +00:00

245 lines
5.7 KiB
TypeScript

import type { RefObject } from 'react';
import { useCallback, useEffect, useState } from 'react';
import type { HorizontalAlignment, Offset, Position, VerticalAlignment } from '@/types/positioning';
type Props = {
verticalAlign: VerticalAlignment;
horizontalAlign: HorizontalAlignment;
offset: Offset;
anchorRef: RefObject<Element>;
overlayRef: RefObject<Element>;
};
// Leave space for box-shadow effect.
const windowSafePadding = 12;
const selectVerticalAlignment = ({
verticalAlign,
verticalTop,
verticalMiddle,
verticalBottom,
overlayHeight,
}: {
verticalAlign: VerticalAlignment;
verticalTop: number;
verticalMiddle: number;
verticalBottom: number;
overlayHeight: number;
}): VerticalAlignment => {
const minY = windowSafePadding;
const maxY = window.innerHeight - windowSafePadding;
const isTopAllowed = verticalTop >= minY;
const isCenterAllowed =
verticalMiddle - overlayHeight / 2 >= minY && verticalMiddle + overlayHeight / 2 <= maxY;
const isBottomAllowed = verticalBottom + overlayHeight <= maxY;
switch (verticalAlign) {
case 'top': {
if (isTopAllowed) {
return 'top';
}
if (isBottomAllowed) {
return 'bottom';
}
if (isCenterAllowed) {
return 'middle';
}
return verticalAlign;
}
case 'middle': {
if (isCenterAllowed) {
return 'middle';
}
if (isTopAllowed) {
return 'top';
}
if (isBottomAllowed) {
return 'bottom';
}
return verticalAlign;
}
case 'bottom': {
if (isBottomAllowed) {
return 'bottom';
}
if (isTopAllowed) {
return 'top';
}
if (isCenterAllowed) {
return 'middle';
}
return verticalAlign;
}
}
};
const selectHorizontalAlignment = ({
horizontalAlign,
horizontalStart,
horizontalCenter,
horizontalEnd,
overlayWidth,
}: {
horizontalAlign: HorizontalAlignment;
horizontalStart: number;
horizontalCenter: number;
horizontalEnd: number;
overlayWidth: number;
}) => {
const minX = windowSafePadding;
const maxX = window.innerWidth - windowSafePadding;
const isStartAllowed = horizontalStart + overlayWidth <= maxX;
const isCenterAllowed =
horizontalCenter - overlayWidth / 2 >= minX && horizontalCenter + overlayWidth / 2 <= maxX;
const isEndAllowed = horizontalEnd >= minX;
switch (horizontalAlign) {
case 'start': {
if (isStartAllowed) {
return 'start';
}
if (isEndAllowed) {
return 'end';
}
if (isCenterAllowed) {
return 'center';
}
return horizontalAlign;
}
case 'center': {
if (isCenterAllowed) {
return 'center';
}
if (isStartAllowed) {
return 'start';
}
if (isEndAllowed) {
return 'end';
}
return horizontalAlign;
}
case 'end': {
if (isEndAllowed) {
return 'end';
}
if (isStartAllowed) {
return 'start';
}
if (isCenterAllowed) {
return 'center';
}
return horizontalAlign;
}
}
};
export default function usePosition({
verticalAlign,
horizontalAlign,
offset,
anchorRef,
overlayRef,
}: Props) {
const [position, setPosition] = useState<Position>();
const [currentVerticalAlign, setCurrentVerticalAlign] = useState(verticalAlign);
const [currentHorizontalAlign, setCurrentHorizontalAlign] = useState(horizontalAlign);
const updatePosition = useCallback(() => {
if (!anchorRef.current || !overlayRef.current) {
return;
}
const anchorRect = anchorRef.current.getBoundingClientRect();
const overlayRect = overlayRef.current.getBoundingClientRect();
const verticalTop = anchorRect.y - overlayRect.height + offset.vertical;
const verticalMiddle =
anchorRect.y + anchorRect.height / 2 - overlayRect.height / 2 + offset.vertical;
const verticalBottom = anchorRect.y + anchorRect.height + offset.vertical;
const verticalPositionMap = {
top: verticalTop,
middle: verticalMiddle,
bottom: verticalBottom,
};
const horizontalStart = anchorRect.x + offset.horizontal;
const horizontalCenter =
anchorRect.x + anchorRect.width / 2 - overlayRect.width / 2 + offset.horizontal;
const horizontalEnd = anchorRect.x + anchorRect.width - overlayRect.width + offset.horizontal;
const horizontalPositionMap = {
start: horizontalStart,
center: horizontalCenter,
end: horizontalEnd,
};
const selectedVerticalAlign = selectVerticalAlignment({
verticalAlign,
verticalTop,
verticalMiddle,
verticalBottom,
overlayHeight: overlayRect.height,
});
const selectedHorizontalAlign = selectHorizontalAlignment({
horizontalAlign,
horizontalStart,
horizontalCenter,
horizontalEnd,
overlayWidth: overlayRect.width,
});
setCurrentVerticalAlign(selectedVerticalAlign);
setCurrentHorizontalAlign(selectedHorizontalAlign);
setPosition({
top: verticalPositionMap[selectedVerticalAlign],
left: horizontalPositionMap[selectedHorizontalAlign],
});
}, [anchorRef, horizontalAlign, offset.vertical, offset.horizontal, overlayRef, verticalAlign]);
useEffect(() => {
updatePosition();
window.addEventListener('resize', updatePosition);
window.addEventListener('scroll', updatePosition);
return () => {
window.removeEventListener('resize', updatePosition);
window.removeEventListener('scroll', updatePosition);
};
}, [updatePosition]);
return {
position,
mutate: updatePosition,
positionState: {
verticalAlign: currentVerticalAlign,
horizontalAlign: currentHorizontalAlign,
},
};
}