From 653eefb9aa7a5a4d7272569a25664849afd5d914 Mon Sep 17 00:00:00 2001 From: Ajay Bura <32841439+ajbura@users.noreply.github.com> Date: Sat, 13 Apr 2024 11:35:07 +0530 Subject: [PATCH] fix: auto fallback position and align --- src/components/pop-out/PopOut.stories.tsx | 13 +- src/components/pop-out/PopOut.tsx | 13 +- src/components/tooltip/Tooltip.stories.tsx | 194 ++++++++++++- src/components/tooltip/Tooltip.tsx | 13 +- src/components/util.ts | 310 ++++++++++++++++----- 5 files changed, 455 insertions(+), 88 deletions(-) diff --git a/src/components/pop-out/PopOut.stories.tsx b/src/components/pop-out/PopOut.stories.tsx index cb545a1..f490a78 100644 --- a/src/components/pop-out/PopOut.stories.tsx +++ b/src/components/pop-out/PopOut.stories.tsx @@ -1,26 +1,27 @@ import FocusTrap from "focus-trap-react"; import React, { useState } from "react"; -import { ComponentMeta } from "@storybook/react"; +import { ComponentMeta, ComponentStory } from "@storybook/react"; import { Text } from "../text"; import { PopOut } from "./PopOut"; import { Menu, MenuItem } from "../menu"; import { Icon, Icons } from "../icon"; import { IconButton } from "../icon-button"; import { config } from "../../theme/config.css"; +import { Box } from "../box"; export default { title: "PopOut", component: PopOut, } as ComponentMeta; -export const Interactive = () => { +const Template: ComponentStory = (args) => { const [open, setOpen] = useState(false); return ( -
+ { )} -
+ ); }; + +export const Interactive = Template.bind({}); diff --git a/src/components/pop-out/PopOut.tsx b/src/components/pop-out/PopOut.tsx index ae09a00..f2c452f 100644 --- a/src/components/pop-out/PopOut.tsx +++ b/src/components/pop-out/PopOut.tsx @@ -47,17 +47,16 @@ export const PopOut = as<"div", PopOutProps>( const css = getRelativeFixedPosition( anchor.getBoundingClientRect(), + baseEl.getBoundingClientRect(), position, align, offset, - alignOffset, - baseEl.getBoundingClientRect() + alignOffset ); - baseEl.style.top = css.top; - baseEl.style.bottom = css.bottom; - baseEl.style.left = css.left; - baseEl.style.right = css.right; - baseEl.style.transform = css.transform; + baseEl.style.top = css.top ?? "unset"; + baseEl.style.bottom = css.bottom ?? "unset"; + baseEl.style.left = css.left ?? "unset"; + baseEl.style.right = css.right ?? "unset"; }, [position, align, offset, alignOffset]); useEffect(() => { diff --git a/src/components/tooltip/Tooltip.stories.tsx b/src/components/tooltip/Tooltip.stories.tsx index f0e7fc2..feb9f7b 100644 --- a/src/components/tooltip/Tooltip.stories.tsx +++ b/src/components/tooltip/Tooltip.stories.tsx @@ -35,19 +35,207 @@ Surface.args = { }; export const Interactive = () => ( - + + + + Top Start + + + } + > + {(ref) => ( + + + + )} + + + + Top Center + + + } + > + {(ref) => ( + + + + )} + + + + Top End + + + } + > + {(ref) => ( + + + + )} + + + + Right Start + + + } + > + {(ref) => ( + + + + )} + + + + Right Center + + + } + > + {(ref) => ( + + + + )} + + + Right End + + + } + > + {(ref) => ( + + + + )} + + + + Bottom Start + + + } + > + {(ref) => ( + + + + )} + + + + Bottom Center + + + } + > + {(ref) => ( + + + + )} + + + + Bottom End + + + } + > + {(ref) => ( + + + + )} + + + + Left Start + + + } + > + {(ref) => ( + + + + )} + + + + Left Center + + + } + > + {(ref) => ( + + + + )} + + - Tooltip is Long + Left End } > {(ref) => ( - + )} diff --git a/src/components/tooltip/Tooltip.tsx b/src/components/tooltip/Tooltip.tsx index 3048f0b..56d1daa 100644 --- a/src/components/tooltip/Tooltip.tsx +++ b/src/components/tooltip/Tooltip.tsx @@ -44,17 +44,16 @@ const useTooltip = ( const tooltipCss = getRelativeFixedPosition( anchor.getBoundingClientRect(), + baseEl.getBoundingClientRect(), position, align, offset, - alignOffset, - baseEl.getBoundingClientRect() + alignOffset ); - baseEl.style.top = tooltipCss.top; - baseEl.style.bottom = tooltipCss.bottom; - baseEl.style.left = tooltipCss.left; - baseEl.style.right = tooltipCss.right; - baseEl.style.transform = tooltipCss.transform; + baseEl.style.top = tooltipCss.top ?? "unset"; + baseEl.style.bottom = tooltipCss.bottom ?? "unset"; + baseEl.style.left = tooltipCss.left ?? "unset"; + baseEl.style.right = tooltipCss.right ?? "unset"; }, [position, align, offset, alignOffset]); useEffect(() => { diff --git a/src/components/util.ts b/src/components/util.ts index ec6d29e..32e3ed8 100644 --- a/src/components/util.ts +++ b/src/components/util.ts @@ -1,80 +1,258 @@ export interface PositionCSS { - top: string; - right: string; - bottom: string; - left: string; - transform: string; + top?: string; + right?: string; + bottom?: string; + left?: string; } export type Position = "Top" | "Right" | "Bottom" | "Left"; export type Align = "Start" | "Center" | "End"; +export type RectCords = { + x: number; + y: number; + width: number; + height: number; +}; + +const canAlignXStart = (anchor: RectCords, target: RectCords, offset: number): boolean => + anchor.x + offset + target.width <= document.documentElement.clientWidth; +const canAlignXCenter = (anchor: RectCords, target: RectCords, offset: number): boolean => { + const xCenter = anchor.x + anchor.width / 2; + const left = xCenter - target.width / 2; + if (left < 0) return false; + + return left + offset + target.width <= document.documentElement.clientWidth; +}; +const canAlignXEnd = (anchor: RectCords, target: RectCords, offset: number): boolean => { + const xEnd = anchor.x + anchor.width; + return xEnd - (target.width + offset) >= 0; +}; +const canAlignYStart = (anchor: RectCords, target: RectCords, offset: number): boolean => + anchor.y + offset + target.height <= document.documentElement.clientHeight; +const canAlignYCenter = (anchor: RectCords, target: RectCords, offset: number): boolean => { + const yCenter = anchor.y + anchor.height / 2; + const top = yCenter - target.height / 2; + if (top < 0) return false; + + return top + offset + target.height <= document.documentElement.clientHeight; +}; +const canAlignYEnd = (anchor: RectCords, target: RectCords, offset: number): boolean => { + const yEnd = anchor.y + anchor.height; + + return yEnd - (target.height + offset) >= 0; +}; + +const alignXStart = (anchor: RectCords, offset: number): PositionCSS => ({ + left: `${anchor.x + offset}px`, +}); +const alignXCenter = (anchor: RectCords, target: RectCords, offset: number): PositionCSS => { + const xCenter = anchor.x + anchor.width / 2; + const left = xCenter - target.width / 2; + + return { + left: `${left + offset}px`, + }; +}; +const alignXEnd = (anchor: RectCords, offset: number): PositionCSS => { + const xEnd = anchor.x + anchor.width; + const right = document.documentElement.clientWidth - xEnd; + + return { + right: `${right + offset}px`, + }; +}; +const alignXAuto = ( + anchor: RectCords, + target: RectCords, + offset: number +): PositionCSS | undefined => { + if (canAlignXCenter(anchor, target, offset)) { + return alignXCenter(anchor, target, offset); + } + if (canAlignXStart(anchor, target, offset)) { + return alignXStart(anchor, offset); + } + if (canAlignXEnd(anchor, target, offset)) { + return alignXEnd(anchor, offset); + } + return undefined; +}; +const alignX = ( + align: Align, + anchor: RectCords, + target: RectCords, + offset: number +): PositionCSS | undefined => { + if (align === "Start" && canAlignXStart(anchor, target, offset)) { + return alignXStart(anchor, offset); + } + if (align === "Center" && canAlignXCenter(anchor, target, offset)) { + return alignXCenter(anchor, target, offset); + } + if (align === "End" && canAlignXEnd(anchor, target, offset)) { + return alignXEnd(anchor, offset); + } + return alignXAuto(anchor, target, offset); +}; + +const alignYStart = (anchor: RectCords, offset: number): PositionCSS => ({ + top: `${anchor.y + offset}px`, +}); +const alignYCenter = (anchor: RectCords, target: RectCords, offset: number): PositionCSS => { + const yCenter = anchor.y + anchor.height / 2; + const top = yCenter - target.height / 2; + + return { + top: `${top + offset}px`, + }; +}; +const alignYEnd = (anchor: RectCords, offset: number): PositionCSS => { + const yEnd = anchor.y + anchor.height; + const bottom = document.documentElement.clientHeight - yEnd; + + return { + bottom: `${bottom + offset}px`, + }; +}; +const alignYAuto = ( + anchor: RectCords, + target: RectCords, + offset: number +): PositionCSS | undefined => { + if (canAlignYCenter(anchor, target, offset)) { + return alignYCenter(anchor, target, offset); + } + if (canAlignYStart(anchor, target, offset)) { + return alignYStart(anchor, offset); + } + if (canAlignYEnd(anchor, target, offset)) { + return alignYEnd(anchor, offset); + } + return undefined; +}; +const alignY = ( + align: Align, + anchor: RectCords, + target: RectCords, + offset: number +): PositionCSS | undefined => { + if (align === "Start" && canAlignYStart(anchor, target, offset)) { + return alignYStart(anchor, offset); + } + if (align === "Center" && canAlignYCenter(anchor, target, offset)) { + return alignYCenter(anchor, target, offset); + } + if (align === "End" && canAlignYEnd(anchor, target, offset)) { + return alignYEnd(anchor, offset); + } + return alignYAuto(anchor, target, offset); +}; + +const canPositionTop = (anchor: RectCords, target: RectCords, offset: number): boolean => + target.height + offset <= anchor.y; +const canPositionRight = (anchor: RectCords, target: RectCords, offset: number): boolean => + anchor.x + anchor.width + offset + target.width <= document.documentElement.clientWidth; +const canPositionBottom = (anchor: RectCords, target: RectCords, offset: number): boolean => + anchor.y + anchor.height + offset + target.height <= document.documentElement.clientHeight; +const canPositionLeft = (anchor: RectCords, target: RectCords, offset: number): boolean => + target.width + offset <= anchor.x; + +const positionTop = (anchor: RectCords, offset: number): PositionCSS => { + const bottom = document.documentElement.clientHeight - anchor.y; + return { + bottom: `${bottom + offset}px`, + }; +}; + +const positionRight = (anchor: RectCords, offset: number): PositionCSS => { + const left = anchor.x + anchor.width; + return { + left: `${left + offset}px`, + }; +}; +const positionBottom = (anchor: RectCords, offset: number): PositionCSS => { + const top = anchor.y + anchor.height; + return { + top: `${top + offset}px`, + }; +}; +const positionLeft = (anchor: RectCords, offset: number): PositionCSS => { + const right = document.documentElement.clientWidth - anchor.x; + return { + right: `${right + offset}px`, + }; +}; +const positionAuto = ( + align: Align, + anchor: RectCords, + target: RectCords, + offset: number, + alignOffset: number +): PositionCSS => { + if (canPositionRight(anchor, target, offset)) { + return { + ...positionRight(anchor, offset), + ...alignY(align, anchor, target, alignOffset), + }; + } + if (canPositionBottom(anchor, target, offset)) { + return { + ...positionBottom(anchor, offset), + ...alignX(align, anchor, target, alignOffset), + }; + } + if (canPositionLeft(anchor, target, offset)) { + return { + ...positionLeft(anchor, offset), + ...alignY(align, anchor, target, alignOffset), + }; + } + if (canPositionTop(anchor, target, offset)) { + return { + ...positionTop(anchor, offset), + ...alignX(align, anchor, target, alignOffset), + }; + } + return { + ...alignX(align, anchor, target, alignOffset), + }; +}; + export const getRelativeFixedPosition = ( - anchorRect: DOMRect, + anchor: RectCords, + target: RectCords, position: Position, align: Align, offset: number, - alignOffset: number, - itemRect: DOMRect + alignOffset: number ): PositionCSS => { - const { clientWidth, clientHeight } = document.documentElement; - - const css = { - top: "unset", - right: "unset", - bottom: "unset", - left: "unset", - transform: "none", - }; + if (position === "Top" && canPositionTop(anchor, target, offset)) { + return { + ...positionTop(anchor, offset), + ...alignX(align, anchor, target, alignOffset), + }; + } + if (position === "Right" && canPositionRight(anchor, target, offset)) { + return { + ...positionRight(anchor, offset), + ...alignY(align, anchor, target, alignOffset), + }; + } + if (position === "Bottom" && canPositionBottom(anchor, target, offset)) { + return { + ...positionBottom(anchor, offset), + ...alignX(align, anchor, target, alignOffset), + }; + } + + if (position === "Left" && canPositionLeft(anchor, target, offset)) { + return { + ...positionLeft(anchor, offset), + ...alignY(align, anchor, target, alignOffset), + }; + } - if (position === "Top" || position === "Bottom") { - const top = anchorRect.top - offset; - const bottom = anchorRect.bottom + offset; - const canPositionTop = top >= itemRect.height; - const canPositionBottom = bottom + itemRect.height <= clientHeight; - - if (position === "Top") { - if (canPositionTop) css.bottom = `${clientHeight - top}px`; - else if (canPositionBottom) css.top = `${bottom}px`; - else css.top = `${offset}px`; - } - if (position === "Bottom") { - if (canPositionBottom) css.top = `${bottom}px`; - else if (canPositionTop) css.bottom = `${clientHeight - top}px`; - else css.top = `${offset}px`; - } - - if (align === "Start") css.left = `${anchorRect.left + alignOffset}px`; - if (align === "Center") { - css.left = `${anchorRect.left + anchorRect.width / 2 + alignOffset}px`; - css.transform = "translateX(-50%)"; - } - if (align === "End") css.right = `${clientWidth - anchorRect.right + alignOffset}px`; - } else { - const left = anchorRect.left - offset; - const right = anchorRect.right + offset; - const canPositionLeft = left >= itemRect.width; - const canPositionRight = right + itemRect.width <= clientWidth; - - if (position === "Left") { - if (canPositionLeft) css.right = `${clientWidth - left}px`; - else if (canPositionRight) css.left = `${right}px`; - else css.left = `${offset}px`; - } - if (position === "Right") { - if (canPositionRight) css.left = `${right}px`; - else if (canPositionLeft) css.right = `${clientWidth - left}px`; - else css.left = `${offset}px`; - } - - if (align === "Start") css.top = `${anchorRect.top + alignOffset}px`; - if (align === "Center") { - css.transform = "translateY(-50%)"; - css.top = `${anchorRect.top + anchorRect.height / 2 + alignOffset}px`; - } - if (align === "End") css.bottom = `${clientHeight - anchorRect.bottom + alignOffset}px`; - } - - return css; + return positionAuto(align, anchor, target, offset, alignOffset); }; export const percent = (min: number, max: number, value: number) =>