1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and affiliates.
    
  3.  *
    
  4.  * This source code is licensed under the MIT license found in the
    
  5.  * LICENSE file in the root directory of this source tree.
    
  6.  *
    
  7.  * @flow
    
  8.  */
    
  9. 
    
  10. import type {ReactContext} from 'shared/ReactTypes';
    
  11. import * as React from 'react';
    
  12. import {
    
  13.   createContext,
    
  14.   useCallback,
    
  15.   useContext,
    
  16.   useMemo,
    
  17.   useReducer,
    
  18.   useRef,
    
  19. } from 'react';
    
  20. import Button from './Button';
    
  21. import {useModalDismissSignal} from './hooks';
    
  22. 
    
  23. import styles from './ModalDialog.css';
    
  24. 
    
  25. type ID = any;
    
  26. 
    
  27. type DIALOG_ACTION_HIDE = {
    
  28.   type: 'HIDE',
    
  29.   id: ID,
    
  30. };
    
  31. type DIALOG_ACTION_SHOW = {
    
  32.   type: 'SHOW',
    
  33.   canBeDismissed?: boolean,
    
  34.   content: React$Node,
    
  35.   id: ID,
    
  36.   title?: React$Node | null,
    
  37. };
    
  38. 
    
  39. type Action = DIALOG_ACTION_HIDE | DIALOG_ACTION_SHOW;
    
  40. 
    
  41. type Dispatch = (action: Action) => void;
    
  42. 
    
  43. type Dialog = {
    
  44.   canBeDismissed: boolean,
    
  45.   content: React$Node | null,
    
  46.   id: ID,
    
  47.   title: React$Node | null,
    
  48. };
    
  49. 
    
  50. type State = {
    
  51.   dialogs: Array<Dialog>,
    
  52. };
    
  53. 
    
  54. type ModalDialogContextType = {
    
  55.   ...State,
    
  56.   dispatch: Dispatch,
    
  57. };
    
  58. 
    
  59. const ModalDialogContext: ReactContext<ModalDialogContextType> =
    
  60.   createContext<ModalDialogContextType>(((null: any): ModalDialogContextType));
    
  61. ModalDialogContext.displayName = 'ModalDialogContext';
    
  62. 
    
  63. function dialogReducer(state: State, action: Action) {
    
  64.   switch (action.type) {
    
  65.     case 'HIDE':
    
  66.       return {
    
  67.         dialogs: state.dialogs.filter(dialog => dialog.id !== action.id),
    
  68.       };
    
  69.     case 'SHOW':
    
  70.       return {
    
  71.         dialogs: [
    
  72.           ...state.dialogs,
    
  73.           {
    
  74.             canBeDismissed: action.canBeDismissed !== false,
    
  75.             content: action.content,
    
  76.             id: action.id,
    
  77.             title: action.title || null,
    
  78.           },
    
  79.         ],
    
  80.       };
    
  81.     default:
    
  82.       throw new Error(`Invalid action "${action.type}"`);
    
  83.   }
    
  84. }
    
  85. 
    
  86. type Props = {
    
  87.   children: React$Node,
    
  88. };
    
  89. 
    
  90. function ModalDialogContextController({children}: Props): React.Node {
    
  91.   const [state, dispatch] = useReducer<State, State, Action>(dialogReducer, {
    
  92.     dialogs: [],
    
  93.   });
    
  94. 
    
  95.   const value = useMemo<ModalDialogContextType>(
    
  96.     () => ({
    
  97.       dialogs: state.dialogs,
    
  98.       dispatch,
    
  99.     }),
    
  100.     [state, dispatch],
    
  101.   );
    
  102. 
    
  103.   return (
    
  104.     <ModalDialogContext.Provider value={value}>
    
  105.       {children}
    
  106.     </ModalDialogContext.Provider>
    
  107.   );
    
  108. }
    
  109. 
    
  110. function ModalDialog(_: {}): React.Node {
    
  111.   const {dialogs, dispatch} = useContext(ModalDialogContext);
    
  112. 
    
  113.   if (dialogs.length === 0) {
    
  114.     return null;
    
  115.   }
    
  116. 
    
  117.   return (
    
  118.     <div className={styles.Background}>
    
  119.       {dialogs.map(dialog => (
    
  120.         <ModalDialogImpl
    
  121.           key={dialog.id}
    
  122.           canBeDismissed={dialog.canBeDismissed}
    
  123.           content={dialog.content}
    
  124.           dispatch={dispatch}
    
  125.           id={dialog.id}
    
  126.           title={dialog.title}
    
  127.         />
    
  128.       ))}
    
  129.     </div>
    
  130.   );
    
  131. }
    
  132. 
    
  133. function ModalDialogImpl({
    
  134.   canBeDismissed,
    
  135.   content,
    
  136.   dispatch,
    
  137.   id,
    
  138.   title,
    
  139. }: {
    
  140.   canBeDismissed: boolean,
    
  141.   content: React$Node | null,
    
  142.   dispatch: Dispatch,
    
  143.   id: ID,
    
  144.   title: React$Node | null,
    
  145. }) {
    
  146.   const dismissModal = useCallback(() => {
    
  147.     if (canBeDismissed) {
    
  148.       dispatch({type: 'HIDE', id});
    
  149.     }
    
  150.   }, [canBeDismissed, dispatch]);
    
  151.   const dialogRef = useRef<HTMLDivElement | null>(null);
    
  152. 
    
  153.   // It's important to trap click events within the dialog,
    
  154.   // so the dismiss hook will use it for click hit detection.
    
  155.   // Because multiple tabs may be showing this ModalDialog,
    
  156.   // the normal `dialog.contains(target)` check would fail on a background tab.
    
  157.   useModalDismissSignal(dialogRef, dismissModal, false);
    
  158. 
    
  159.   // Clicks on the dialog should not bubble.
    
  160.   // This way we can dismiss by listening to clicks on the background.
    
  161.   const handleDialogClick = (event: any) => {
    
  162.     event.stopPropagation();
    
  163. 
    
  164.     // It is important that we don't also prevent default,
    
  165.     // or clicks within the dialog (e.g. on links) won't work.
    
  166.   };
    
  167. 
    
  168.   return (
    
  169.     <div ref={dialogRef} className={styles.Dialog} onClick={handleDialogClick}>
    
  170.       {title !== null && <div className={styles.Title}>{title}</div>}
    
  171.       {content}
    
  172.       {canBeDismissed && (
    
  173.         <div className={styles.Buttons}>
    
  174.           <Button
    
  175.             autoFocus={true}
    
  176.             className={styles.Button}
    
  177.             onClick={dismissModal}>
    
  178.             Okay
    
  179.           </Button>
    
  180.         </div>
    
  181.       )}
    
  182.     </div>
    
  183.   );
    
  184. }
    
  185. 
    
  186. export {ModalDialog, ModalDialogContext, ModalDialogContextController};