LCOV - code coverage report
Current view: top level - pkg/lib - dialogs.tsx Coverage Total Hit
Test: cockpit Lines: 97.5 % 40 39
Test Date: 2026-07-17 07:33:01

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2022 Red Hat, Inc.
       3              :  * SPDX-License-Identifier: LGPL-2.1-or-later
       4              :  */
       5              : 
       6              : /* DIALOG PRESENTATION PROTOCOL
       7              :  *
       8              :  * Example:
       9              :  *
      10              :  * import { WithDialogs, useDialogs } from "dialogs.jsx";
      11              :  *
      12              :  * const App = () =>
      13              :  *   <WithDialogs>
      14              :  *     <Page>
      15              :  *       <ExampleButton />
      16              :  *     </Page>
      17              :  *   </WithDialogs>;
      18              :  *
      19              :  * const ExampleButton = () => {
      20              :  *   const Dialogs = useDialogs();
      21              :  *   return <Button onClick={() => Dialogs.show(<MyDialog />)}>Open dialog</Button>;
      22              :  * };
      23              :  *
      24              :  * const MyDialog = () => {
      25              :  *   const Dialogs = useDialogs();
      26              :  *   return (
      27              :  *     <Modal title="My dialog"
      28              :  *            isOpen
      29              :  *            onClose={Dialogs.close}>
      30              :  *       <p>Hello!</p>
      31              :  *     </Modal>);
      32              :  * };
      33              :  *
      34              :  * This does two things: It maintains the state of whether the dialog
      35              :  * is open, and it does it high up in the DOM, in a stable place.
      36              :  * Even if ExampleButton is no longer part of the DOM, the dialog will
      37              :  * stay open and remain usable.
      38              :  *
      39              :  * The "WithDialogs" component enables all its children to show
      40              :  * dialogs.  Such a dialog will stay open as long as the WithDialogs
      41              :  * component itself is mounted.  Thus, you should put the WithDialogs
      42              :  * component somewhere high up in your component tree, maybe even as
      43              :  * the very top-most component.
      44              :  *
      45              :  * If your Cockpit application has multiple pages and navigation
      46              :  * between these pages is controlled by the browser URL, then each of
      47              :  * these pages should have its own WithDialogs wrapper. This way, a
      48              :  * dialog opened on one page closes when the user navigates away from
      49              :  * that page. To make sure that React maintains separate states for
      50              :  * WithDialogs components, give them unique "key" properties.
      51              :  *
      52              :  * A component that wants to show a dialogs needs to get hold of the
      53              :  * current "Dialogs" context and then call it's "show" method.  For a
      54              :  * function component the Dialogs context is returned by
      55              :  * "useDialogs()", as shown above in the example.
      56              :  *
      57              :  * A class component can declare a static context type and then use
      58              :  * "this.context" to find the Dialogs object:
      59              :  *
      60              :  * import { DialogsContext } from "dialogs.jsx";
      61              :  *
      62              :  * class ExampleButton extends React.Component {
      63              :  *   static contextType = DialogsContext;
      64              :  *
      65              :  *   function render() {
      66              :  *     const Dialogs = this.context;
      67              :  *     return <Button onClick={() => Dialogs.show(<MyDialog />)}>Open dialog</Button>;
      68              :  *   }
      69              :  * }
      70              :  *
      71              :  *
      72              :  * - Dialogs.show(component)
      73              :  *
      74              :  * Calling "Dialogs.show" will render the given component as a direct
      75              :  * child of the inner-most enclosing "WithDialogs" component.  The
      76              :  * component is of course intended to be a dialog, such as
      77              :  * Patternfly's "Modal".  There is only ever one of these; a second
      78              :  * call to "show" is considered a bug and "Dialogs.close" must be called first.
      79              :  *
      80              :  * - Dialogs.close()
      81              :  *
      82              :  * Calling "Dialogs.close()" will close the currently open Dialog.  It can only
      83              :  * be used with dialogs shown by `Dialogs.show()`.
      84              :  *
      85              :  * - Dialogs.run(component, {... props})
      86              :  *
      87              :  * Shows a dialog and asynchronously waits for it to close.  This creates and
      88              :  * shows a MyDialog with the given properties, plus a special "dialogResult"
      89              :  * property which has .resolve() and .reject() methods on it.  Calling either
      90              :  * of those will resolve the promise returned by Dialogs.run() accordingly,
      91              :  * closing the dialog in the process.  The created dialog cannot be closed with
      92              :  * Dialogs.close(). See the example:
      93              :  *
      94              :  * const MyDialog = ({ title, dialogResult }) => {
      95              :  *     return (
      96              :  *         <Modal title={title}>
      97              :  *             <Button onClick={() => dialogResult.resolve("yes")}>Yes</Button>
      98              :  *             <Button onClick={() => dialogResult.resolve("no")}>No</Button>
      99              :  *         </Modal>
     100              :  *     );
     101              :  * };
     102              :  *
     103              :  * const AsyncDialogExample = () => {
     104              :  *     const Dialogs = useDialogs();
     105              :  *
     106              :  *     const clicked = async () => {
     107              :  *         try {
     108              :  *             const result = await Dialogs.run(MyDialog, { title: "Example" });
     109              :  *             console.log(result);
     110              :  *         } catch (err) {
     111              :  *         }
     112              :  *     };
     113              :  *
     114              :  *     return <Button onClick={clicked}>Open dialog</Button>;
     115              :  * };
     116              :  *
     117              :  * - Dialogs.isActive()
     118              :  *
     119              :  * Returns `true` if a dialog is currently being shown.
     120              :  *
     121              :  */
     122              : 
     123          555 : import React, { useContext, useState, useRef } from "react";
     124              : 
     125              : export interface DialogResult<T> {
     126              :     resolve(value: T): void;
     127              :     reject(exc: unknown): void;
     128              : }
     129              : 
     130              : export interface Dialogs {
     131              :     show(dialog: React.ReactNode): void;
     132              :     close(): void;
     133              :     run<T, P>(component: React.ComponentType<P & { dialogResult: DialogResult<T> }>, properties: P): Promise<T>;
     134              :     isActive(): boolean;
     135              : }
     136              : 
     137          555 : export const DialogsContext = React.createContext<Dialogs | null>(null);
     138          534 : export const useDialogs = () => {
     139          534 :     const dialogs = useContext(DialogsContext);
     140           90 :     if (dialogs === null) {
     141           90 :         throw new Error("useDialogs can only be called inside of <WithDialogs/>");
     142           90 :     }
     143          534 :     return dialogs;
     144          534 : };
     145              : 
     146          555 : export const WithDialogs = ({ children } : { children: React.ReactNode }) => {
     147          555 :     const [dialog, setDialog] = useState<React.ReactNode>(null);
     148              :     type State = "close" | "show" | "run";
     149          555 :     const shown = useRef<State>("close");
     150              : 
     151           85 :     function transition(expected: State, to: State, arg: React.ReactNode = null) {
     152           85 :         if (shown.current !== expected)
     153            5 :             throw new Error(`Dialogs.${to}(${JSON.stringify(arg)}) called, but that's only valid ` +
     154            5 :                             `after .${expected}(), current dialog is ${JSON.stringify(dialog)}.`);
     155           85 :         shown.current = to;
     156           85 :         setDialog(arg);
     157           85 :     }
     158              : 
     159          555 :     const Dialogs: Dialogs = {
     160           78 :         show: (component: React.ReactNode) => transition("close", "show", component),
     161           74 :         close: () => transition("show", "close"),
     162            7 :         run: async (component, props) => {
     163            7 :             try {
     164            7 :                 return await new Promise((resolve, reject) => {
     165            7 :                     transition("close", "run",
     166            7 :                                React.createElement(component, { ...props, dialogResult: { resolve, reject } }));
     167            7 :                 });
     168            7 :             } finally {
     169            7 :                 transition("run", "close");
     170            7 :             }
     171            7 :         },
     172            0 :         isActive: () => dialog !== null
     173          555 :     };
     174              : 
     175          555 :     return (
     176          555 :         <DialogsContext.Provider value={Dialogs}>
     177          555 :             {children}
     178          555 :             {dialog}
     179          555 :         </DialogsContext.Provider>);
     180          555 : };
        

Generated by: LCOV version 2.0-1