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;
}