diff --git a/src/components/as.tsx b/src/components/as.tsx index 265844e..ec2c083 100644 --- a/src/components/as.tsx +++ b/src/components/as.tsx @@ -1,9 +1,12 @@ import { ElementType, forwardRef, ReactElement } from "react"; import { AsComponentProps, RefOfType } from "./types"; -export const as = >( - fc: (props: AsComponentProps & ExtraProps, ref: RefOfType) => ReactElement +export const as = ( + fc: ( + props: Omit, keyof ExtraProps> & ExtraProps, + ref: RefOfType + ) => ReactElement ) => - forwardRef(fc) as unknown as ( - props: AsComponentProps & ExtraProps + forwardRef(fc) as unknown as ( + props: Omit, keyof ExtraProps> & ExtraProps ) => ReactElement; diff --git a/src/components/pop-out/PopOut.tsx b/src/components/pop-out/PopOut.tsx index d703ef8..08b7108 100644 --- a/src/components/pop-out/PopOut.tsx +++ b/src/components/pop-out/PopOut.tsx @@ -3,85 +3,98 @@ import React, { ReactNode, useCallback, useEffect, - useLayoutEffect, useRef, + useState, } from "react"; import { config } from "../../theme"; +import { as } from "../as"; import { Portal } from "../portal"; -import { Align, getRelativeFixedPosition, Position } from "../util"; +import { Align, getRelativeFixedPosition, Position, PositionCSS } from "../util"; export interface PopOutProps { open: boolean; position?: Position; align?: Align; offset?: number; + alignOffset?: number; content: ReactNode; children: (anchorRef: MutableRefObject) => ReactNode; } -export const PopOut = ({ - open, - position = "bottom", - align = "center", - offset = 10, - content, - children, -}: PopOutProps) => { - const anchorRef = useRef(null); - const popOutRef = useRef(null); +export const PopOut = as<"div", PopOutProps>( + ( + { + as: AsPopOut = "div", + open, + position = "bottom", + align = "center", + offset = 10, + alignOffset = 0, + content, + children, + style, + ...props + }, + ref + ) => { + const anchorRef = useRef(null); + const [popOutPosition, setPopOutPosition] = useState(); - const positionPopOut = useCallback(() => { - const anchor = anchorRef.current as HTMLElement; - const popOutEl = popOutRef.current; + const positionPopOut = useCallback(() => { + const anchor = anchorRef.current as HTMLElement; - const css = getRelativeFixedPosition(anchor.getBoundingClientRect(), position, align, offset); - if (popOutEl) { - popOutEl.style.top = css.top; - popOutEl.style.right = css.right; - popOutEl.style.bottom = css.bottom; - popOutEl.style.left = css.left; - popOutEl.style.transform = css.transform; - } - }, [position, align, offset]); + const css = getRelativeFixedPosition( + anchor.getBoundingClientRect(), + position, + align, + offset, + alignOffset + ); + setPopOutPosition(css); + }, [position, align, offset, alignOffset]); - useEffect(() => { - window.addEventListener("resize", positionPopOut); - return () => { - window.removeEventListener("resize", positionPopOut); - }; - }, [positionPopOut]); + useEffect(() => { + window.addEventListener("resize", positionPopOut); + return () => { + window.removeEventListener("resize", positionPopOut); + }; + }, [positionPopOut]); - useLayoutEffect(() => { - if (open) positionPopOut(); - }, [open, positionPopOut]); + useEffect(() => { + if (open) positionPopOut(); + }, [open, positionPopOut]); - return ( - <> - {children(anchorRef as MutableRefObject)} - - {open && ( -
-
+ {children(anchorRef as MutableRefObject)} + + {open && popOutPosition && ( + - {content} -
-
- )} -
- - ); -}; +
+ {content} +
+ + )} + + + ); + } +); diff --git a/src/components/tooltip/Tooltip.tsx b/src/components/tooltip/Tooltip.tsx index 25046a5..2d92df0 100644 --- a/src/components/tooltip/Tooltip.tsx +++ b/src/components/tooltip/Tooltip.tsx @@ -3,7 +3,7 @@ import React, { MutableRefObject, ReactNode, useEffect, useRef, useState } from import { config } from "../../theme"; import { as } from "../as"; import { Portal } from "../portal"; -import { Align, getRelativeFixedPosition, Position } from "../util"; +import { Align, getRelativeFixedPosition, Position, PositionCSS } from "../util"; import * as css from "./Tooltip.css"; export const Tooltip = as<"div", css.TooltipVariants>( @@ -16,15 +16,18 @@ export const Tooltip = as<"div", css.TooltipVariants>( ) ); -const useTooltip = (position: Position, align: Align, offset: number, delay: number) => { +const useTooltip = ( + position: Position, + align: Align, + offset: number, + alignOffset: number, + delay: number +) => { const triggerRef = useRef(null); - const tooltipRef = useRef(null); - - const [open, setOpen] = useState(false); + const [tooltipPosition, setTooltipPosition] = useState(); useEffect(() => { const trigger = triggerRef.current as HTMLElement; - const tooltip = tooltipRef.current; let timeoutId: number | undefined; const openTooltip = (evt: Event) => { @@ -33,35 +36,24 @@ const useTooltip = (position: Position, align: Align, offset: number, delay: num trigger.getBoundingClientRect(), position, align, - offset + offset, + alignOffset ); - if (tooltip) { - tooltip.style.top = pos.top; - tooltip.style.right = pos.right; - tooltip.style.bottom = pos.bottom; - tooltip.style.left = pos.left; - tooltip.style.transform = pos.transform; - } - if (evt.type === "focus") setOpen(true); - else timeoutId = window.setTimeout(() => setOpen(true), delay); + if (evt.type === "focus") setTooltipPosition(pos); + else timeoutId = window.setTimeout(() => setTooltipPosition(pos), delay); }; const closeTooltip = () => { clearTimeout(timeoutId); timeoutId = undefined; - setOpen(false); + setTooltipPosition(undefined); }; const onKeyDown = (evt: KeyboardEvent) => { - if ( - evt.key === "Escape" && - document.activeElement === trigger && - tooltip && - tooltip.children.length > 0 - ) { + if (evt.key === "Escape" && document.activeElement === trigger) { evt.preventDefault(); clearTimeout(timeoutId); - setOpen(false); + setTooltipPosition(undefined); } }; @@ -80,12 +72,11 @@ const useTooltip = (position: Position, align: Align, offset: number, delay: num document.removeEventListener("keydown", onKeyDown); trigger?.removeEventListener("click", closeTooltip); }; - }, [position, align, offset, delay]); + }, [position, align, offset, alignOffset, delay]); return { - open, + tooltipPosition, triggerRef, - tooltipRef, }; }; @@ -93,38 +84,53 @@ interface TooltipProviderProps { position?: Position; align?: Align; offset?: number; + alignOffset?: number; delay?: number; tooltip: ReactNode; children: (triggerRef: MutableRefObject) => ReactNode; } -export const TooltipProvider = ({ - position = "top", - align = "center", - offset = 10, - delay = 200, - tooltip, - children, -}: TooltipProviderProps) => { - const { open, tooltipRef, triggerRef } = useTooltip(position, align, offset, delay); +export const TooltipProvider = as<"div", TooltipProviderProps>( + ( + { + as: AsTooltipProvider = "div", + position = "top", + align = "center", + offset = 10, + alignOffset = 0, + delay = 200, + tooltip, + children, + style, + ...props + }, + ref + ) => { + const { tooltipPosition, triggerRef } = useTooltip(position, align, offset, alignOffset, delay); - return ( - <> - {children(triggerRef as MutableRefObject)} - -
- {open && tooltip} -
-
- - ); -}; + return ( + <> + {children(triggerRef as MutableRefObject)} + {tooltipPosition && ( + + + {tooltip} + + + )} + + ); + } +); diff --git a/src/components/types.ts b/src/components/types.ts index 8f4f889..d4eb05a 100644 --- a/src/components/types.ts +++ b/src/components/types.ts @@ -1,10 +1,4 @@ -import { - ComponentPropsWithoutRef, - ComponentPropsWithRef, - ElementType, - PropsWithChildren, - ReactElement, -} from "react"; +import { ComponentPropsWithRef, ElementType, PropsWithChildren } from "react"; export type MainColor = "Primary" | "Secondary" | "Success" | "Warning" | "Critical"; @@ -25,12 +19,5 @@ type AsProp = { }; export type AsComponentProps = PropsWithChildren< - ComponentPropsWithoutRef & AsProp -> & { - ref?: RefOfType; -}; - -export type AsComponentType< - DefaultElement extends ElementType, - ExtraProps = Record -> = (props: AsComponentProps & ExtraProps) => ReactElement | null; + ComponentPropsWithRef & AsProp +>; diff --git a/src/components/util.ts b/src/components/util.ts index ac7eee7..bc44cd9 100644 --- a/src/components/util.ts +++ b/src/components/util.ts @@ -12,7 +12,8 @@ export const getRelativeFixedPosition = ( domRect: DOMRect, position: Position, align: Align, - offset: number + offset: number, + alignOffset: number ): PositionCSS => { const { clientWidth, clientHeight } = document.body; @@ -28,22 +29,22 @@ export const getRelativeFixedPosition = ( if (position === "top") css.bottom = `${clientHeight - domRect.top + offset}px`; else css.top = `${domRect.bottom + offset}px`; - if (align === "start") css.left = `${domRect.left}px`; + if (align === "start") css.left = `${domRect.left + alignOffset}px`; if (align === "center") { - css.left = `${domRect.left + domRect.width / 2}px`; + css.left = `${domRect.left + domRect.width / 2 + alignOffset}px`; css.transform = "translateX(-50%)"; } - if (align === "end") css.right = `${clientWidth - domRect.right}px`; + if (align === "end") css.right = `${clientWidth - domRect.right + alignOffset}px`; } else { if (position === "right") css.left = `${domRect.right + offset}px`; else css.right = `${clientWidth - domRect.left + offset}px`; - if (align === "start") css.top = `${domRect.top}px`; + if (align === "start") css.top = `${domRect.top + alignOffset}px`; if (align === "center") { css.transform = "translateY(-50%)"; - css.top = `${domRect.top + domRect.height / 2}px`; + css.top = `${domRect.top + domRect.height / 2 + alignOffset}px`; } - if (align === "end") css.bottom = `${clientHeight - domRect.bottom}px`; + if (align === "end") css.bottom = `${clientHeight - domRect.bottom + alignOffset}px`; } return css;