LCOV - code coverage report
Current view: top level - dashql-app/src/ui/foundations - overlay.tsx Coverage Total Hit
Test: DashQL 915828b Lines: 100.0 % 153 153
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : import * as React from "react";
       2              : 
       3              : import { Portal } from './portal.js';
       4              : import { classNames } from '../../utils/classnames.js';
       5              : import { useOpenAndCloseFocus } from './focus.js';
       6              : import { useOnOutsideClick } from './outside_click.js';
       7              : import { useKeyEvents } from '../../utils/key_events.js';
       8              : import { AnchorSide } from './anchored_position.js';
       9              : 
      10              : import * as styles from './overlay.module.css';
      11              : 
      12              : interface UseOverlayArgs {
      13              :     ignoreClickRefs?: React.RefObject<Element | null>[];
      14              :     initialFocusRef?: React.RefObject<HTMLElement | null>;
      15              :     returnFocusRef?: React.RefObject<HTMLElement | null>;
      16              :     onEscape: (e: KeyboardEvent) => void;
      17              :     onClickOutside: (e: TouchEvent | MouseEvent) => void;
      18              :     overlayRef?: React.RefObject<HTMLDivElement | null>;
      19              :     preventFocusOnOpen?: boolean;
      20              : }
      21              : 
      22            1 : export function useOverlay(args: UseOverlayArgs): React.RefObject<HTMLDivElement | null> {
      23            1 :     const ownOverlayRef = React.useRef<HTMLDivElement | null>(null);
      24            1 :     const overlayRef = args.overlayRef ?? ownOverlayRef;
      25            1 :     useOpenAndCloseFocus({ containerRef: overlayRef, returnFocusRef: args.returnFocusRef, initialFocusRef: args.initialFocusRef, preventFocusOnOpen: args.preventFocusOnOpen });
      26            1 :     useOnOutsideClick({ containerRef: overlayRef, ignoreClickRefs: args.ignoreClickRefs, onClickOutside: args.onClickOutside })
      27            1 :     useKeyEvents([
      28            1 :         {
      29            1 :             key: "Escape",
      30            1 :             ctrlKey: false,
      31            1 :             callback: (e: KeyboardEvent) => {
      32            1 :                 args.onEscape(e);
      33            1 :                 e.stopPropagation();
      34            1 :                 e.preventDefault();
      35            1 :             }
      36            1 :         }
      37            1 :     ]);
      38            1 :     return overlayRef;
      39            1 : }
      40              : 
      41            1 : export enum OverlaySize {
      42            1 :     UNSPECIFIED,
      43            1 :     AUTO,
      44            1 :     XS,
      45            1 :     S,
      46            1 :     M,
      47            1 :     L,
      48            1 :     XL,
      49            1 :     XXL,
      50            1 : }
      51              : 
      52            1 : const HEIGHT_STYLES: (string | null)[] = [
      53            1 :     null,
      54            1 :     styles.height_auto,
      55            1 :     styles.height_xs,
      56            1 :     styles.height_s,
      57            1 :     styles.height_m,
      58            1 :     styles.height_l,
      59            1 :     styles.height_xl,
      60            1 :     null,
      61            1 : ];
      62            1 : const MAX_HEIGHT_STYLES: (string | null)[] = [
      63            1 :     null,
      64            1 :     null,
      65            1 :     styles.max_height_xs,
      66            1 :     styles.max_height_s,
      67            1 :     styles.max_height_m,
      68            1 :     styles.max_height_l,
      69            1 :     styles.max_height_xl,
      70            1 :     null,
      71            1 : ];
      72              : 
      73            1 : const WIDTH_STYLES: (string | null)[] = [
      74            1 :     null,
      75            1 :     styles.width_auto,
      76            1 :     null,
      77            1 :     styles.width_s,
      78            1 :     styles.width_m,
      79            1 :     styles.width_l,
      80            1 :     styles.width_xl,
      81            1 :     styles.width_xxl,
      82            1 : ];
      83            1 : const MIN_WIDTH_STYLES: (string | null)[] = [
      84            1 :     null,
      85            1 :     null,
      86            1 :     null,
      87            1 :     styles.min_width_s,
      88            1 :     styles.min_width_m,
      89            1 :     styles.min_width_l,
      90            1 :     styles.min_width_xl,
      91            1 :     styles.min_width_xxl,
      92            1 : ];
      93            1 : const MAX_WIDTH_STYLES: (string | null)[] = [
      94            1 :     null,
      95            1 :     null,
      96            1 :     null,
      97            1 :     styles.max_width_s,
      98            1 :     styles.max_width_m,
      99            1 :     styles.max_width_l,
     100            1 :     styles.max_width_xl,
     101            1 :     styles.max_width_xxl,
     102            1 : ];
     103              : 
     104            1 : export enum OverlayVisibility {
     105            1 :     Hidden,
     106            1 :     Visible
     107            1 : }
     108              : 
     109            1 : function getSlideAnimationStartingVector(anchorSide?: AnchorSide): { x: number; y: number } {
     110            1 :     switch (anchorSide) {
     111            1 :         case AnchorSide.OutsideTop:
     112            1 :         case AnchorSide.InsideTop:
     113            1 :             return { x: 0, y: 1 };
     114            1 :         case AnchorSide.OutsideRight:
     115            1 :         case AnchorSide.InsideRight:
     116            1 :             return { x: -1, y: 0 };
     117            1 :         case AnchorSide.OutsideLeft:
     118            1 :         case AnchorSide.InsideLeft:
     119            1 :             return { x: 1, y: 0 };
     120            1 :         case AnchorSide.OutsideBottom:
     121            1 :         case AnchorSide.InsideBottom:
     122            1 :             return { x: 0, y: -1 };
     123            1 :     }
     124            1 :     return { x: 0, y: 0 }
     125            1 : }
     126              : 
     127              : export interface OverlayProps {
     128              :     ignoreClickRefs?: React.RefObject<Element | null>[];
     129              :     initialFocusRef?: React.RefObject<HTMLElement | null>;
     130              :     returnFocusRef?: React.RefObject<HTMLElement | null>;
     131              :     onClickOutside: (e: MouseEvent | TouchEvent) => void;
     132              :     onEscape: (e: KeyboardEvent) => void;
     133              :     visibility?: OverlayVisibility;
     134              :     preventFocusOnOpen?: boolean;
     135              :     portalContainerName?: string;
     136              :     top?: React.CSSProperties['top'];
     137              :     left?: React.CSSProperties['left'];
     138              :     right?: React.CSSProperties['right'];
     139              :     bottom?: React.CSSProperties['bottom'];
     140              :     position?: React.CSSProperties['position'];
     141              :     width?: OverlaySize;
     142              :     height?: OverlaySize;
     143              :     minWidth?: OverlaySize;
     144              :     maxWidth?: OverlaySize;
     145              :     maxHeight?: OverlaySize;
     146              :     anchorSide?: AnchorSide;
     147              :     centered?: boolean;
     148              :     /// Fill the window except for the inset, overrides the width and height sizes
     149              :     expanded?: boolean;
     150              :     children?: React.ReactNode;
     151              :     role?: React.AriaRole;
     152              : }
     153              : 
     154            1 : const SLIDE_ANIMATION_DISTANCE = 8;
     155              : 
     156            1 : export const Overlay = React.forwardRef((props: OverlayProps, forwardedRef: React.ForwardedRef<HTMLDivElement>) => {
     157              :     // Use a ref as forwarded ref
     158            1 :     const overlayRef = React.useRef<HTMLDivElement>(null);
     159            1 :     React.useImperativeHandle(forwardedRef, () => overlayRef.current!);
     160              : 
     161            1 :     const width = WIDTH_STYLES[props.width ?? OverlaySize.UNSPECIFIED];
     162            1 :     const height = props.height ? HEIGHT_STYLES[props.height] : null;
     163            1 :     const minWidth = props.minWidth ? MIN_WIDTH_STYLES[props.minWidth] : null;
     164            1 :     const maxWidth = props.maxWidth ? MAX_WIDTH_STYLES[props.maxWidth] : null;
     165            1 :     const maxHeight = props.maxHeight ? MAX_HEIGHT_STYLES[props.maxHeight] : null;
     166              : 
     167            1 :     useOverlay({
     168            1 :         overlayRef,
     169            1 :         returnFocusRef: props.returnFocusRef,
     170            1 :         onEscape: props.onEscape,
     171            1 :         ignoreClickRefs: props.ignoreClickRefs,
     172            1 :         onClickOutside: props.onClickOutside,
     173            1 :         initialFocusRef: props.initialFocusRef,
     174            1 :         preventFocusOnOpen: props.preventFocusOnOpen,
     175            1 :     });
     176              : 
     177            1 :     React.useLayoutEffect(() => {
     178            1 :         if (!overlayRef.current?.animate || props.visibility === OverlayVisibility.Hidden) {
     179            1 :             return
     180              :         }
     181            1 :         const { x, y } = getSlideAnimationStartingVector(props.anchorSide);
     182            1 :         if (x || y) {
     183            1 :             overlayRef.current.animate(
     184            1 :                 { transform: [`translate(${SLIDE_ANIMATION_DISTANCE * x}px, ${SLIDE_ANIMATION_DISTANCE * y}px)`, `translate(0, 0)`] },
     185            1 :                 {
     186            1 :                     duration: 200,
     187            1 :                     easing: "cubic-bezier(0.33, 1, 0.68, 1)",
     188            1 :                 },
     189            1 :             )
     190              :         }
     191            1 :     }, [props.anchorSide, props.visibility])
     192              : 
     193            1 :     const overlay = (
     194            1 :         <div
     195            1 :             className={classNames(styles.overlay, width, height, minWidth, maxWidth, maxHeight, { [styles.expanded]: props.expanded === true })}
     196            1 :             ref={overlayRef}
     197            1 :             role={props.role}
     198            1 :             data-expanded={props.expanded === true}
     199            1 :             data-electron-drag-region="false"
     200            1 :             style={props.centered ? undefined : {
     201            1 :                 position: props.position,
     202            1 :                 top: props.top,
     203            1 :                 right: props.right,
     204            1 :                 bottom: props.bottom,
     205            1 :                 left: props.left
     206            1 :             }}
     207              :         >
     208            1 :             {props.children}
     209            1 :         </div>
     210              :     );
     211              : 
     212            1 :     return (
     213            1 :         <Portal containerName={props.portalContainerName}>
     214            1 :             {props.centered ? (
     215            1 :                 <div className={styles.centered_container}>{overlay}</div>
     216            1 :             ) : overlay}
     217            1 :         </Portal>
     218              :     );
     219            1 : });
        

Generated by: LCOV version 2.0-1