diff --git a/platform/ui/index.js b/platform/ui/index.js index d9fe40b36..6990c8d25 100644 --- a/platform/ui/index.js +++ b/platform/ui/index.js @@ -1,4 +1,10 @@ import * as utils from './src/utils/'; +import { + ModalProvider, + ModalConsumer, + useModal, + withModal, +} from './src/contextProviders'; export { Button, @@ -12,7 +18,5 @@ export { ThemeWrapper, Typography, } from './src/components'; - -export { utils }; - export { StudyList } from './src/views'; +export { ModalProvider, ModalConsumer, useModal, withModal, utils }; diff --git a/platform/ui/src/contextProviders/ModalProvider.js b/platform/ui/src/contextProviders/ModalProvider.js new file mode 100644 index 000000000..e8d65635d --- /dev/null +++ b/platform/ui/src/contextProviders/ModalProvider.js @@ -0,0 +1,134 @@ +import React, { + useState, + createContext, + useContext, + useEffect, + useCallback, +} from 'react'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; + +const ModalContext = createContext(null); +const { Provider } = ModalContext; + +export const useModal = () => useContext(ModalContext); + +/** + * UI Modal + * + * @typedef {Object} ModalProps + * @property {ReactElement|HTMLElement} [content=null] Modal content. + * @property {Object} [contentProps=null] Modal content props. + * @property {boolean} [shouldCloseOnEsc=false] Modal is dismissible via the esc key. + * @property {boolean} [isOpen=true] Make the Modal visible or hidden. + * @property {boolean} [closeButton=true] Should the modal body render the close button. + * @property {string} [title=null] Should the modal render the title independently of the body content. + * @property {string} [customClassName=null] The custom class to style the modal. + */ + +const ModalProvider = ({ children, modal: Modal, service }) => { + const DEFAULT_OPTIONS = { + content: null, + contentProps: null, + shouldCloseOnEsc: false, + isOpen: true, + closeButton: true, + title: null, + customClassName: '', + }; + + const [options, setOptions] = useState(DEFAULT_OPTIONS); + + /** + * Sets the implementation of a modal service that can be used by extensions. + * + * @returns void + */ + useEffect(() => { + if (service) { + service.setServiceImplementation({ hide, show }); + } + }, [hide, service, show]); + + /** + * Show the modal and override its configuration props. + * + * @param {ModalProps} props { content, contentProps, shouldCloseOnEsc, isOpen, closeButton, title, customClassName } + * @returns void + */ + const show = useCallback(props => setOptions({ ...options, ...props }), [ + options, + ]); + + /** + * Hide the modal and set its properties to default. + * + * @returns void + */ + const hide = useCallback(() => setOptions(DEFAULT_OPTIONS), [ + DEFAULT_OPTIONS, + ]); + + const { + content: ModalContent, + contentProps, + isOpen, + title, + customClassName, + shouldCloseOnEsc, + closeButton, + } = options; + + return ( + + {ModalContent && ( + + + + )} + {children} + + ); +}; + +/** + * Higher Order Component to use the modal methods through a Class Component. + * + * @returns + */ +export const withModal = Component => { + return function WrappedComponent(props) { + const { show, hide } = useModal(); + return ; + }; +}; + +ModalProvider.defaultProps = { + service: null, +}; + +ModalProvider.propTypes = { + children: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + ]).isRequired, + modal: PropTypes.oneOfType([ + PropTypes.arrayOf(PropTypes.node), + PropTypes.node, + PropTypes.func, + ]).isRequired, + service: PropTypes.shape({ + setServiceImplementation: PropTypes.func, + }), +}; + +export default ModalProvider; + +export const ModalConsumer = ModalContext.Consumer; diff --git a/platform/ui/src/contextProviders/index.js b/platform/ui/src/contextProviders/index.js new file mode 100644 index 000000000..f2e6811fb --- /dev/null +++ b/platform/ui/src/contextProviders/index.js @@ -0,0 +1,6 @@ +export { + default as ModalProvider, + useModal, + withModal, + ModalConsumer, +} from './ModalProvider.js';