Skip to content

Commit

Permalink
fix: tooltip and popout (#18)
Browse files Browse the repository at this point in the history
* feat: align offset prop to tooltip & popout

* fix: pointer event on popout overlay

* fix: as component types

* fix: use as component for tooltip & popout
  • Loading branch information
ajbura authored May 11, 2023
1 parent f4e1249 commit 2665446
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 144 deletions.
11 changes: 7 additions & 4 deletions src/components/as.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { ElementType, forwardRef, ReactElement } from "react";
import { AsComponentProps, RefOfType } from "./types";

export const as = <T extends ElementType, ExtraProps = Record<string, unknown>>(
fc: (props: AsComponentProps<T> & ExtraProps, ref: RefOfType<T>) => ReactElement
export const as = <T extends ElementType, ExtraProps = unknown>(
fc: (
props: Omit<AsComponentProps<T>, keyof ExtraProps> & ExtraProps,
ref: RefOfType<T>
) => ReactElement
) =>
forwardRef(fc) as unknown as <E extends ElementType = T>(
props: AsComponentProps<E> & ExtraProps
forwardRef(fc) as unknown as <E extends ElementType>(
props: Omit<AsComponentProps<E>, keyof ExtraProps> & ExtraProps
) => ReactElement;
133 changes: 73 additions & 60 deletions src/components/pop-out/PopOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<null>) => ReactNode;
}
export const PopOut = ({
open,
position = "bottom",
align = "center",
offset = 10,
content,
children,
}: PopOutProps) => {
const anchorRef = useRef<unknown>(null);
const popOutRef = useRef<HTMLDivElement>(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<unknown>(null);
const [popOutPosition, setPopOutPosition] = useState<PositionCSS>();

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<null>)}
<Portal>
{open && (
<div
style={{
position: "fixed",
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: config.zIndex.Max,
}}
>
<div
ref={popOutRef}
return (
<>
{children(anchorRef as MutableRefObject<null>)}
<Portal>
{open && popOutPosition && (
<AsPopOut
style={{
display: "inline-block",
position: "fixed",
maxWidth: "100vw",
top: 0,
right: 0,
bottom: 0,
left: 0,
zIndex: config.zIndex.Max,
...style,
}}
{...props}
ref={ref}
>
{content}
</div>
</div>
)}
</Portal>
</>
);
};
<div
style={{
display: "inline-block",
position: "fixed",
maxWidth: "100vw",
...popOutPosition,
}}
>
{content}
</div>
</AsPopOut>
)}
</Portal>
</>
);
}
);
120 changes: 63 additions & 57 deletions src/components/tooltip/Tooltip.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>(
Expand All @@ -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<unknown>(null);
const tooltipRef = useRef<HTMLDivElement>(null);

const [open, setOpen] = useState(false);
const [tooltipPosition, setTooltipPosition] = useState<PositionCSS>();

useEffect(() => {
const trigger = triggerRef.current as HTMLElement;
const tooltip = tooltipRef.current;
let timeoutId: number | undefined;

const openTooltip = (evt: Event) => {
Expand All @@ -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);
}
};

Expand All @@ -80,51 +72,65 @@ 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,
};
};

interface TooltipProviderProps {
position?: Position;
align?: Align;
offset?: number;
alignOffset?: number;
delay?: number;
tooltip: ReactNode;
children: (triggerRef: MutableRefObject<null>) => 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<null>)}
<Portal>
<div
role="tooltip"
ref={tooltipRef}
style={{
display: "inline-block",
position: "fixed",
maxWidth: "100vw",
zIndex: config.zIndex.Max,
pointerEvents: "none",
}}
>
{open && tooltip}
</div>
</Portal>
</>
);
};
return (
<>
{children(triggerRef as MutableRefObject<null>)}
{tooltipPosition && (
<Portal>
<AsTooltipProvider
role="tooltip"
style={{
display: "inline-block",
position: "fixed",
maxWidth: "100vw",
zIndex: config.zIndex.Max,
pointerEvents: "none",
...tooltipPosition,
...style,
}}
{...props}
ref={ref}
>
{tooltip}
</AsTooltipProvider>
</Portal>
)}
</>
);
}
);
19 changes: 3 additions & 16 deletions src/components/types.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -25,12 +19,5 @@ type AsProp<E extends ElementType> = {
};

export type AsComponentProps<E extends ElementType> = PropsWithChildren<
ComponentPropsWithoutRef<E> & AsProp<E>
> & {
ref?: RefOfType<E>;
};

export type AsComponentType<
DefaultElement extends ElementType,
ExtraProps = Record<string, unknown>
> = (props: AsComponentProps<DefaultElement> & ExtraProps) => ReactElement | null;
ComponentPropsWithRef<E> & AsProp<E>
>;
Loading

0 comments on commit 2665446

Please sign in to comment.