diff --git a/platform/ui/src/components/snackbar/Snackbar.css b/platform/ui/src/components/snackbar/Snackbar.css new file mode 100644 index 000000000..8b0ee787e --- /dev/null +++ b/platform/ui/src/components/snackbar/Snackbar.css @@ -0,0 +1,196 @@ +.sb-container { + position: fixed; + width: var(--snackbar-size); + padding: 20px; + z-index: var(--snackbar-zIndex); + box-sizing: border-box; + height: auto; +} + +.sb-topLeft { + top: 0; + bottom: auto; + left: 0; + right: auto; +} + +.sb-topCenter { + left: 50%; + top: 0; + bottom: auto; + transform: translateX(-50%); +} + +.sb-topRight { + top: 0; + bottom: auto; + left: auto; + right: 0; +} + +.sb-bottomLeft { + top: auto; + bottom: 0px; + left: 0px; + right: auto; +} + +.sb-bottomCenter { + left: 50%; + bottom: 0; + top: auto; + transform: translateX(-50%); +} + +.sb-bottomRight { + top: auto; + bottom: 0px; + left: auto; + right: 0px; + margin: 10px 0 0; +} + +.sb-topLeft .sb-item, +.sb-topCenter .sb-item, +.sb-topRight .sb-item { + margin: 10px 0 0; +} + +.sb-bottomLeft .sb-item, +.sb-bottomCenter .sb-item, +.sb-bottomRight .sb-item { + margin: 0 0 10px; +} + +.sb-closeBtn { + height: 20px; + opacity: 1; + overflow: hidden; + padding: 2px; + text-align: center; + text-shadow: none; + width: 20px; + cursor: pointer; + position: absolute; + right: 5px; + top: 5px; + transition: all 0.3s ease; + background: rgba(255, 255, 255, 0.6); + border-radius: 100%; +} + +.sb-closeBtn:hover { + background: #fff; +} + +.sb-closeIcon { + display: block; + font-size: 0; + height: 100%; + line-height: 0; + overflow: hidden; + position: relative; + width: 100%; +} + +.sb-closeIcon:after, +.sb-closeIcon:before { + content: ' '; + display: block; + height: 2px; + transition: all 0.3s ease; + width: 12px; + background-color: #222; + opacity: 1; + position: absolute; +} + +.sb-closeIcon:before { + left: 4px; + top: 3px; + + transform: rotate(45deg); + transform-origin: 0px 50%; +} + +.sb-closeIcon:after { + right: 3px; + top: 5px; + transform: rotate(-45deg); + transform-origin: calc(100% - 3px) 50%; +} + +.sb-title { + font-size: 16px; + font-weight: bold; +} + +.sb-message { + font-size: 14px; + word-break: break-all; +} + +.sb-item { + position: relative; + transition: height 300ms ease; + animation: fadein 1s; + padding: 20px; + color: white; + overflow: hidden; + border-radius: 4px; + box-shadow: 0 6px 10px 0 rgba(0, 0, 0, 0.2), 0 1px 18px 0 rgba(0, 0, 0, 0.12), + 0 3px 5px -1px rgba(0, 0, 0, 0.14); +} + +.sb-item a { + color: white; + text-decoration: underline; +} + +.sb-container .sb-hidden { + padding-top: 0; + padding-bottom: 0; + margin-bottom: 0; + height: 0; + opacity: 0; + transition: all 300ms ease; +} + +.sb-success { + background-color: var(--snackbar-success); +} + +.sb-error { + background-color: var(--snackbar-error); +} + +.sb-warning { + background-color: var(--snackbar-warning); +} + +.sb-info { + background-color: var(--snackbar-info); +} + +@keyframes fadein { + from { + opacity: 0; + top: 30px; + } + to { + opacity: 1; + top: 0; + } +} + +/* Internet Explorer */ +@-ms-keyframes fadein { + from { + opacity: 0; + top: 30px; + } + to { + opacity: 1; + top: 0; + } +} diff --git a/platform/ui/src/components/snackbar/SnackbarContainer.js b/platform/ui/src/components/snackbar/SnackbarContainer.js new file mode 100644 index 000000000..53dcce052 --- /dev/null +++ b/platform/ui/src/components/snackbar/SnackbarContainer.js @@ -0,0 +1,53 @@ +import React from 'react'; +import SnackbarItem from './SnackbarItem'; +import './Snackbar.css'; +import { useSnackbarContext } from '../../utils/SnackbarProvider'; + +const SnackbarContainer = () => { + const { snackbarItems, hide } = useSnackbarContext(); + + const renderItem = item => { + return ; + }; + + if (!snackbarItems) { + return null; + } + + const renderItems = () => { + const items = { + topLeft: [], + topCenter: [], + topRight: [], + bottomLeft: [], + bottomCenter: [], + bottomRight: [], + }; + + snackbarItems.map(item => { + items[item.position].push(item); + }); + + return ( +
+ {Object.keys(items).map(pos => { + if (!items[pos].length) { + return null; + } + + return ( +
+ {items[pos].map(item => ( +
{renderItem(item)}
+ ))} +
+ ); + })} +
+ ); + }; + + return <>{renderItems()}; +}; + +export default SnackbarContainer; diff --git a/platform/ui/src/components/snackbar/SnackbarItem.js b/platform/ui/src/components/snackbar/SnackbarItem.js new file mode 100644 index 000000000..40c43697e --- /dev/null +++ b/platform/ui/src/components/snackbar/SnackbarItem.js @@ -0,0 +1,31 @@ +import React, { useState, useEffect } from 'react'; + +const SnackbarItem = ({ options, onClose }) => { + const handleClose = () => { + onClose(options.id); + }; + + useEffect(() => { + if (options.autoClose) { + setTimeout(() => { + handleClose(); + }, options.duration); + } + }, []); + + return ( +
+ + x + + {options.title &&
{options.title}
} + {options.message &&
{options.message}
} +
+ ); +}; + +export default SnackbarItem; diff --git a/platform/ui/src/components/snackbar/SnackbarTypes.js b/platform/ui/src/components/snackbar/SnackbarTypes.js new file mode 100644 index 000000000..3dffcb5c9 --- /dev/null +++ b/platform/ui/src/components/snackbar/SnackbarTypes.js @@ -0,0 +1,6 @@ +export default { + INFO: 'info', + WARNING: 'warning', + SUCCESS: 'success', + ERROR: 'error', +}; diff --git a/platform/ui/src/index.js b/platform/ui/src/index.js index 357d18c61..82a221070 100644 --- a/platform/ui/src/index.js +++ b/platform/ui/src/index.js @@ -32,6 +32,10 @@ import { ScrollableArea } from './ScrollableArea/ScrollableArea.js'; import Toolbar from './viewer/Toolbar.js'; import ToolbarButton from './viewer/ToolbarButton.js'; import ViewerbaseDragDropContext from './utils/viewerbaseDragDropContext.js'; +import SnackbarProvider, { + useSnackbarContext, + withSnackbar, +} from './utils/SnackbarProvider'; export { ICONS, @@ -68,4 +72,7 @@ export { UserPreferences, UserPreferencesModal, ViewerbaseDragDropContext, + SnackbarProvider, + useSnackbarContext, + withSnackbar, }; diff --git a/platform/ui/src/utils/SnackbarProvider.js b/platform/ui/src/utils/SnackbarProvider.js new file mode 100644 index 000000000..929e768cb --- /dev/null +++ b/platform/ui/src/utils/SnackbarProvider.js @@ -0,0 +1,102 @@ +import React, { useState, createContext, useContext } from 'react'; +import SnackbarContainer from '../components/snackbar/SnackbarContainer'; +import SnackbarTypes from '../components/snackbar/SnackbarTypes'; + +const SnackbarContext = createContext(null); + +export const useSnackbarContext = () => useContext(SnackbarContext); + +const SnackbarProvider = ({ children }) => { + const DEFAULT_OPTIONS = { + title: '', + message: '', + duration: 5000, + autoClose: true, + position: 'bottomRight', + type: SnackbarTypes.INFO, + }; + + const [count, setCount] = useState(1); + const [snackbarItems, setSnackbarItems] = useState([]); + + const show = options => { + if (!options || (!options.title && !options.message)) { + console.warn( + 'Snackbar cannot be rendered without required parameters: title | message' + ); + + return null; + } + + const newItem = { + ...DEFAULT_OPTIONS, + ...options, + id: count, + visible: true, + }; + + setSnackbarItems(state => [...state, newItem]); + setCount(count + 1); + }; + + const hide = id => { + const hideItem = items => { + const newItems = items.map(item => { + if (item.id === id) { + item.visible = false; + } + + return item; + }); + + return newItems; + }; + + setSnackbarItems(state => hideItem(state)); + + setTimeout(() => { + setSnackbarItems(state => [...state.filter(item => item.id !== id)]); + }, 1000); + }; + + const hideAll = () => { + // reset count + setCount(1); + + // remove all items from array + setSnackbarItems(() => []); + }; + + /** + * expose snackbar methods to window for debug purposes + * TODO: Check if it's really necessary + */ + window.snackbar = { + show, + hide, + hideAll, + }; + + return ( + + {!!snackbarItems && } + {children} + + ); +}; + +/** + * + * High Order Component to use the snackbar methods through a Class Component + * + */ +export const withSnackbar = Component => { + return function WrappedComponent(props) { + const snackbarContext = { + ...useSnackbarContext(), + }; + return ; + }; +}; + +export default SnackbarProvider; diff --git a/platform/viewer/src/App.js b/platform/viewer/src/App.js index 64ef69516..9ddb43679 100644 --- a/platform/viewer/src/App.js +++ b/platform/viewer/src/App.js @@ -27,6 +27,7 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { getActiveContexts } from './store/layout/selectors.js'; import i18n from '@ohif/i18n'; import store from './store'; +import { SnackbarProvider } from '@ohif/ui'; // Contexts import WhiteLabellingContext from './context/WhiteLabellingContext'; @@ -99,7 +100,9 @@ class App extends Component { - + + + @@ -116,7 +119,9 @@ class App extends Component { - + + + diff --git a/platform/viewer/src/variables.css b/platform/viewer/src/variables.css index 09d0e2728..5fe5ef59f 100644 --- a/platform/viewer/src/variables.css +++ b/platform/viewer/src/variables.css @@ -1,6 +1,6 @@ -/* Sizes */ - :root { + /* Sizes */ + --top-bar-height: 40px; --top-bar-expanded-height: 160px; --toolbar-height: 78px; @@ -9,19 +9,26 @@ --right-sidepanel-menu-width: 323px; --study-list-padding: 8%; --study-list-padding-medium-screen: 10px; -} + --snackbar-size: 344px; -/* Transitions */ + /* Transitions */ -:root { --transition-duration: 0.3s; --transition-effect: ease; --sidepanel-transition: all 0.3s ease; -} -/* Thicknesses */ + /* Thicknesses */ -:root { --viewport-border-thickness: 1px; --ui-border-thickness: 1px; + + /** Colors **/ + + --snackbar-success: rgba(94, 164, 0, 0.9); + --snackbar-error: rgba(236, 61, 61, 0.9); + --snackbar-warning: rgba(235, 173, 23, 0.9); + --snackbar-info: rgba(54, 156, 199, 0.9); + + /** z-Index **/ + --snackbar-zIndex: 10; }