Skip to content

Commit

Permalink
Allow multiple dialogs to be open at once #894
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Sep 25, 2024
1 parent 609db79 commit 67ea77c
Show file tree
Hide file tree
Showing 17 changed files with 206 additions and 101 deletions.
6 changes: 3 additions & 3 deletions browser/data-browser/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { initBugsnag } from './helpers/loggingHandlers';
import HotKeysWrapper from './components/HotKeyWrapper';
import { AppSettingsContextProvider } from './helpers/AppSettings';
import CrashPage from './views/CrashPage';
import { DialogContainer } from './components/Dialog/DialogContainer';
import { DialogGlobalContextProvider } from './components/Dialog/DialogGlobalContextProvider';
import { registerHandlers } from './handlers';
import { ErrorBoundary } from './views/ErrorPage';
import { NetworkIndicator } from './components/NetworkIndicator';
Expand Down Expand Up @@ -108,7 +108,7 @@ function App(): JSX.Element {
<Toaster />
<MetaSetter />
<DropdownContainer>
<DialogContainer>
<DialogGlobalContextProvider>
<PopoverContainer>
<DropdownContainer>
<NewResourceUIProvider>
Expand All @@ -120,7 +120,7 @@ function App(): JSX.Element {
</DropdownContainer>
</PopoverContainer>
<NetworkIndicator />
</DialogContainer>
</DialogGlobalContextProvider>
</DropdownContainer>
</FormValidationContextProvider>
</ErrBoundary>
Expand Down
20 changes: 0 additions & 20 deletions browser/data-browser/src/components/Dialog/DialogContainer.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import {
createContext,
useCallback,
useContext,
useEffect,
useId,
useMemo,
useRef,
useState,
type FC,
type PropsWithChildren,
type RefObject,
} from 'react';
import { styled } from 'styled-components';

interface DialogGlobalContext {
openDialogs: string[];
setDialogOpen: (id: string, open: boolean) => void;
portal: RefObject<HTMLDivElement>;
}

export const DialogContext = createContext<DialogGlobalContext>(null!);

export const DialogGlobalContextProvider: FC<PropsWithChildren> = ({
children,
}) => {
const [openDialogs, setOpenDialogs] = useState<string[]>([]);
const portalRef = useRef<HTMLDivElement>(null);

const setDialogOpen = useCallback((id: string, open: boolean) => {
if (open) {
setOpenDialogs(prev => {
if (prev.includes(id)) {
return prev;
}

return [...prev, id];
});
} else {
setOpenDialogs(prev => prev.filter(dialogId => dialogId !== id));
}
}, []);

const context = useMemo(
() => ({ openDialogs, setDialogOpen, portal: portalRef }),
[openDialogs, setDialogOpen, portalRef],
);

return (
<DialogContext.Provider value={context}>
{children}
<StyledDiv ref={portalRef}></StyledDiv>
</DialogContext.Provider>
);
};

export function useDialogGlobalContext(open: boolean) {
const id = useId();
const { openDialogs, setDialogOpen, ...context } = useContext(DialogContext);

const isTopLevel = openDialogs.at(-1) === id;

useEffect(() => {
setDialogOpen(id, open);
}, [open, id]);

return {
isTopLevel,
...context,
};
}

const StyledDiv = styled.div`
display: contents;
`;
19 changes: 13 additions & 6 deletions browser/data-browser/src/components/Dialog/dialogContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,12 @@ import {
Dispatch,
FC,
PropsWithChildren,
RefObject,
SetStateAction,
useContext,
useMemo,
useState,
} from 'react';

export const DialogPortalContext = createContext<RefObject<HTMLDivElement>>(
null!,
);

interface DialogTreeContext {
inDialog: boolean;
hasOpenInnerPopup: boolean;
Expand All @@ -29,6 +24,7 @@ export const DialogTreeContext = createContext<DialogTreeContext>({
export const DialogTreeContextProvider: FC<PropsWithChildren> = ({
children,
}) => {
// Keep track of whether there is an open inner popup. This can be used to disable dismissal controls in dialogs while a popup is open.
const [hasOpenInnerPopup, setHasOpenInnerPopup] = useState<boolean>(false);

const context = useMemo(
Expand All @@ -37,7 +33,7 @@ export const DialogTreeContextProvider: FC<PropsWithChildren> = ({
hasOpenInnerPopup,
setHasOpenInnerPopup,
}),
[hasOpenInnerPopup],
[hasOpenInnerPopup, setHasOpenInnerPopup],
);

return (
Expand All @@ -47,6 +43,17 @@ export const DialogTreeContextProvider: FC<PropsWithChildren> = ({
);
};

export function useDialogTreeInfo() {
const { inDialog, hasOpenInnerPopup, setHasOpenInnerPopup } =
useContext(DialogTreeContext);

return {
inDialog,
hasOpenInnerPopup,
setHasOpenInnerPopup,
};
}

export function useDialogTreeContext() {
return useContext(DialogTreeContext);
}
34 changes: 18 additions & 16 deletions browser/data-browser/src/components/Dialog/index.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import {
useCallback,
useContext,
useEffect,
useLayoutEffect,
useRef,
} from 'react';
import { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook';
import { FaTimes } from 'react-icons/fa';
Expand All @@ -16,12 +10,12 @@ import { DropdownContainer } from '../Dropdown/DropdownContainer';
import { PopoverContainer } from '../Popover';
import { Slot } from '../Slot';
import {
DialogPortalContext,
DialogTreeContextProvider,
useDialogTreeContext,
} from './dialogContext';
import { useDialog } from './useDialog';
import { useControlLock } from '../../hooks/useControlLock';
import { useDialogGlobalContext } from './DialogGlobalContextProvider';

export interface InternalDialogProps {
show: boolean;
Expand Down Expand Up @@ -70,17 +64,17 @@ type DialogSlotComponent = React.FC<React.PropsWithChildren<DialogSlotProps>>;
* ```
*/
export function Dialog(props: React.PropsWithChildren<InternalDialogProps>) {
const portalRef = useContext(DialogPortalContext);
const { portal } = useDialogGlobalContext(false);

if (!portalRef.current) {
if (!portal.current) {
return null;
}

return createPortal(
<DialogTreeContextProvider>
<InnerDialog {...props} />
</DialogTreeContextProvider>,
portalRef.current,
portal.current,
);
}

Expand All @@ -94,6 +88,7 @@ const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
const dialogRef = useRef<HTMLDialogElement>(null);
const innerDialogRef = useRef<HTMLDivElement>(null);
const { hasOpenInnerPopup } = useDialogTreeContext();
const { isTopLevel } = useDialogGlobalContext(show);

useControlLock(show);

Expand All @@ -105,14 +100,20 @@ const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
React.MouseEventHandler<HTMLDialogElement>
>(
e => {
if (!isTopLevel) {
// Don't react to closing events if the dialog is not on top.

return;
}

if (
!innerDialogRef.current?.contains(e.target as HTMLElement) &&
innerDialogRef.current !== e.target
) {
cancelDialog();
}
},
[innerDialogRef.current, cancelDialog],
[innerDialogRef.current, cancelDialog, isTopLevel],
);

// Close the dialog when the escape key is pressed
Expand All @@ -121,7 +122,7 @@ const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
() => {
cancelDialog();
},
{ enabled: show && !hasOpenInnerPopup },
{ enabled: show && !hasOpenInnerPopup && isTopLevel },
);

// When closing the `data-closing` attribute must be set before rendering so the animation has started when the regular useEffect is called.
Expand Down Expand Up @@ -158,6 +159,7 @@ const InnerDialog: React.FC<React.PropsWithChildren<InternalDialogProps>> = ({
ref={dialogRef}
onMouseDown={handleOutSideClick}
$width={width}
data-top-level={isTopLevel}
>
<StyledInnerDialog ref={innerDialogRef}>
<PopoverContainer>
Expand Down Expand Up @@ -294,7 +296,7 @@ const StyledDialog = styled.dialog<{ $width?: CSS.Property.Width }>`
&::backdrop {
background-color: rgba(0, 0, 0, 0);
backdrop-filter: blur(0px);
backdrop-filter: blur(0px) grayscale(0%);
transition:
background-color ${ANIM_SPEED} ease-out,
backdrop-filter ${ANIM_SPEED} ease-out;
Expand All @@ -316,13 +318,13 @@ const StyledDialog = styled.dialog<{ $width?: CSS.Property.Width }>`
&[open]::backdrop {
background-color: rgba(0, 0, 0, 0.383);
backdrop-filter: blur(5px);
backdrop-filter: blur(5px) grayscale(90%);
animation: ${fadeInBackground} ${ANIM_SPEED} ease-out;
}
&[data-closing='true']::backdrop {
background-color: rgba(0, 0, 0, 0);
backdrop-filter: blur(0px);
backdrop-filter: blur(0px) grayscale(0%);
}
@media (max-width: ${DIALOG_MEDIA_BREAK_POINT}) {
Expand Down
10 changes: 5 additions & 5 deletions browser/data-browser/src/components/ImageViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { useContext, useState } from 'react';
import { useState } from 'react';
import { createPortal } from 'react-dom';
import { useHotkeys } from 'react-hotkeys-hook';

import { styled } from 'styled-components';
import { DialogPortalContext } from './Dialog/dialogContext';
import { useFileImageTransitionStyles } from '../views/File/useFileImageTransitionStyles';
import { useDialogGlobalContext } from './Dialog/DialogGlobalContextProvider';

interface ImageViewerProps {
src: string;
Expand All @@ -21,12 +21,12 @@ export function ImageViewer({
subject,
}: ImageViewerProps): JSX.Element {
const [showFull, setShowFull] = useState(false);
const portalRef = useContext(DialogPortalContext);
const { portal } = useDialogGlobalContext(false);

const transitionStyles = useFileImageTransitionStyles(subject);
useHotkeys('esc', () => setShowFull(false), { enabled: showFull });

if (!portalRef.current) {
if (!portal.current) {
return <></>;
}

Expand All @@ -51,7 +51,7 @@ export function ImageViewer({
<Viewer>
<img src={src} alt={alt ?? ''} data-test={`image-viewer`} />
</Viewer>,
portalRef.current,
portal.current,
)}
</WrapperButton>
);
Expand Down
2 changes: 1 addition & 1 deletion browser/data-browser/src/components/NetworkIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function NetworkIndicator() {
}, [isOnline]);

return (
<Wrapper shown={!isOnline}>
<Wrapper shown={!isOnline} aria-hidden={isOnline}>
<MdSignalWifiOff title='No Internet Connection.' />
</Wrapper>
);
Expand Down
4 changes: 2 additions & 2 deletions browser/data-browser/src/components/Popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
import * as RadixPopover from '@radix-ui/react-popover';
import { styled, keyframes } from 'styled-components';
import { transparentize } from 'polished';
import { useDialogTreeContext } from './Dialog/dialogContext';
import { useDialogTreeInfo } from './Dialog/dialogContext';
import { useControlLock } from '../hooks/useControlLock';

export interface PopoverProps {
Expand All @@ -38,7 +38,7 @@ export function Popover({
onOpenChange,
Trigger,
}: PropsWithChildren<PopoverProps>): JSX.Element {
const { setHasOpenInnerPopup } = useDialogTreeContext();
const { setHasOpenInnerPopup } = useDialogTreeInfo();
const containerRef = useContext(PopoverContainerContext);

const container = containerRef.current ?? undefined;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,10 @@ export function FilePicker({

return (
<Wrapper>
<VisuallyHidden>
<VisuallyHidden aria-hidden='true'>
{value}
<input
aria-hidden
tabIndex={-1}
type='text'
defaultValue={value ?? ''}
required={required}
Expand Down
Loading

0 comments on commit 67ea77c

Please sign in to comment.