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 : });
|