/*
* This file is part of Cockpit.
*
* Copyright (C) 2022 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
/* DIALOG PRESENTATION PROTOCOL
*
* Example:
*
* import { WithDialogs, useDialogs } from "dialogs.jsx";
*
* const App = () =>
*
*
*
*
* ;
*
* const ExampleButton = () => {
* const Dialogs = useDialogs();
* return ;
* };
*
* const MyDialog = () => {
* const Dialogs = useDialogs();
* return (
*
*
Hello!
* );
* };
*
* This does two things: It maintains the state of whether the dialog
* is open, and it does it high up in the DOM, in a stable place.
* Even if ExampleButton is no longer part of the DOM, the dialog will
* stay open and remain useable.
*
* The "WithDialogs" component enables all its children to show
* dialogs. Such a dialog will stay open as long as the WithDialogs
* component itself is mounted. Thus, you should put the WithDialogs
* component somewhere high up in your component tree, maybe even as
* the very top-most component.
*
* If your Cockpit application has multiple pages and navigation
* between these pages is controlled by the browser URL, then each of
* these pages should have its own WithDialogs wrapper. This way, a
* dialog opened on one page closes when the user navigates away from
* that page. To make sure that React maintains separate states for
* WithDialogs components, give them unique "key" properties.
*
* A component that wants to show a dialogs needs to get hold of the
* current "Dialogs" context and then call it's "show" method. For a
* function component the Dialogs context is returned by
* "useDialogs()", as shown above in the example.
*
* A class component can declare a static context type and then use
* "this.context" to find the Dialogs object:
*
* import { DialogsContext } from "dialogs.jsx";
*
* class ExampleButton extends React.Component {
* static contextType = DialogsContext;
*
* function render() {
* const Dialogs = this.context;
* return ;
* }
* }
*
*
* - Dialogs.show(component)
*
* Calling "Dialogs.show" will render the given component as a direct
* child of the inner-most enclosing "WithDialogs" component. The
* component is of course intended to be a dialog, such as
* Patternfly's "Modal". There is only ever one of these; a second
* call to "show" will remove the previously rendered component.
* Passing "null" will remove the currently rendered componenet, if any.
*
* - Dialogs.close()
*
* Same as "Dialogs.show(null)".
*/
import React, { useContext, useRef, useState } from "react";
export const DialogsContext = React.createContext();
export const useDialogs = () => useContext(DialogsContext);
export const WithDialogs = ({ children }) => {
const is_open = useRef(false); // synchronous
const [dialog, setDialog] = useState(null);
const Dialogs = {
show: component => {
if (component && is_open.current)
console.error("Dialogs.show() called for",
JSON.stringify(component),
"while a dialog is already open:",
JSON.stringify(dialog));
is_open.current = !!component;
setDialog(component);
},
close: () => {
is_open.current = false;
setDialog(null);
},
isActive: () => dialog !== null
};
return (
{children}
{dialog}
);
};