diff --git a/lerna.json b/lerna.json
index 4e4a23a2f..177e0e12f 100644
--- a/lerna.json
+++ b/lerna.json
@@ -8,6 +8,7 @@
"ignore": [
"terra-clinical-app-delegate",
"terra-clinical-application",
+ "terra-clinical-modal-manager",
"terra-clinical-site",
"terra-clinical-slide-group"
]
diff --git a/packages/terra-clinical-app-delegate/docs/README.md b/packages/terra-clinical-app-delegate/docs/README.md
index f8cfdb2f4..ad61ad72c 100644
--- a/packages/terra-clinical-app-delegate/docs/README.md
+++ b/packages/terra-clinical-app-delegate/docs/README.md
@@ -7,6 +7,7 @@ their consuming Containers/Applications.
- Install with [npmjs](https://www.npmjs.com):
- `npm install terra-clinical-app-delegate`
+ - `yarn add terra-clinical-app-delegate`
## Usage
diff --git a/packages/terra-clinical-app-delegate/package.json b/packages/terra-clinical-app-delegate/package.json
index a8c15eca6..d2e958d68 100644
--- a/packages/terra-clinical-app-delegate/package.json
+++ b/packages/terra-clinical-app-delegate/package.json
@@ -11,6 +11,7 @@
"keywords": [
"Cerner",
"Terra",
+ "Clinical",
"terra-clinical-app-delegate",
"AppDelegate",
"UI"
diff --git a/packages/terra-clinical-application/docs/README.md b/packages/terra-clinical-application/docs/README.md
index 8b8250b46..376ed4191 100644
--- a/packages/terra-clinical-application/docs/README.md
+++ b/packages/terra-clinical-application/docs/README.md
@@ -9,6 +9,7 @@ given to the Application should be able to handle that `app` prop appropriately.
- Install with [npmjs](https://www.npmjs.com):
- `npm install terra-clinical-application`
+ - `yarn add terra-clinical-application`
## Usage
diff --git a/packages/terra-clinical-application/package.json b/packages/terra-clinical-application/package.json
index 43e9112d2..f3569071b 100644
--- a/packages/terra-clinical-application/package.json
+++ b/packages/terra-clinical-application/package.json
@@ -11,6 +11,7 @@
"keywords": [
"Cerner",
"Terra",
+ "Clinical",
"terra-clinical-application",
"Application",
"UI"
diff --git a/packages/terra-clinical-modal-manager/.npmignore b/packages/terra-clinical-modal-manager/.npmignore
new file mode 100644
index 000000000..c7ab43d44
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/.npmignore
@@ -0,0 +1,4 @@
+*.log
+node_modules
+src
+target
diff --git a/packages/terra-clinical-modal-manager/LICENSE b/packages/terra-clinical-modal-manager/LICENSE
new file mode 100644
index 000000000..6b0b1270f
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/LICENSE
@@ -0,0 +1,203 @@
+
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
diff --git a/packages/terra-clinical-modal-manager/NOTICE b/packages/terra-clinical-modal-manager/NOTICE
new file mode 100644
index 000000000..6ce917d94
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/NOTICE
@@ -0,0 +1,13 @@
+Copyright 2017 Cerner Innovation, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/packages/terra-clinical-modal-manager/README.md b/packages/terra-clinical-modal-manager/README.md
new file mode 100644
index 000000000..f45a0e27b
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/README.md
@@ -0,0 +1,25 @@
+# Terra Clinical Modal Manager
+
+
+[![NPM version](http://img.shields.io/npm/v/terra-clinical-modal-manager.svg)](https://www.npmjs.org/package/terra-clinical-modal-manager)
+[![Build Status](https://travis-ci.org/cerner/terra-clinical.svg?branch=master)](https://travis-ci.org/cerner/terra-clinical)
+
+The ModalManager is a Redux-backed Container component that presents a single or multiple components using the `terra-modal`.
+
+- [Getting Started](#getting-started)
+- [Documentation](https://github.com/cerner/terra-clinical/tree/master/packages/terra-clinical-modal-manager/docs)
+- [LICENSE](#license)
+
+## Getting Started
+
+- Install from [npmjs](https://www.npmjs.com): `npm install terra-clinical-modal-manager`
+
+## LICENSE
+
+Copyright 2017 Cerner Innovation, Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
diff --git a/packages/terra-clinical-modal-manager/docs/README.md b/packages/terra-clinical-modal-manager/docs/README.md
new file mode 100644
index 000000000..f8b335fe9
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/docs/README.md
@@ -0,0 +1,125 @@
+# Terra Clinical Modal Manager
+
+The ModalManager is a Redux-backed Container component that dynamically presents components in a Terra Modal.
+
+## Getting Started
+
+- Install with [npmjs](https://www.npmjs.com):
+ - `npm install terra-clinical-modal-manager`
+ - `yarn add terra-clinical-modal-manager`
+
+## Prerequisites
+
+Since ModalManager manages its state using Redux, its reducer must be included when the Redux store is initially created. To make
+this easier, the ModalManager exports a `reducers` object that can be used with `combineReducers` or otherwise used to
+construct the root reducer function of an application.
+
+## Usage
+
+It works like this:
+* One or many components are provided to the ModalManager as children.
+* The ModalManager clones those children and adds an AppDelegate prop to each.
+* The added AppDelegate's `disclose` function, when called with a `preferredType` of `'modal'`, will dispatch the ModalManager's `OPEN` action.
+* The ModalManager will use the data from the `OPEN` action to open the modal and present the specified component within it.
+* The modally-presented component will also recieve an AppDelegate prop configured to further manipulate the modal state.
+
+Components presented in the Modal still have the ability to disclose additional modal content; the ModalManager will maintain both components
+in a stack and give the top-most component the APIs necessary to navigate back (through its AppDelegate).
+
+The disclose APIs for the ModalManager children follow the standard AppDelegate disclose API, with the only addition being a 'size' property that
+will determine the size of the modal.
+
+```jsx
+app.disclose({
+ preferredType: 'modal',
+ size: 'small',
+ content: {...},
+})
+```
+
+|Key|Type|Description|
+|---|---|---|
+|preferredType|String|A String describing the component's desired disclosure method. Should be 'modal' if ModalManager usage is desired.|
+|content|Object|An Object containing data describing the component that is to be disclosed. See AppDelegate documentation for more.|
+|size|String|The desired modal size. One of: `tiny`, `small`, `medium`, `large`, `huge`.|
+
+A more thorough example would look something like this:
+
+```jsx
+// DemoApplication.jsx
+
+import React from 'react';
+import { createStore, combineReducers } from 'redux';
+import { Provider } from 'react-redux';
+
+import Application from 'terra-clinical-application';
+import ModalManager, { reducers as modalManagerReducers } from 'terra-clinical-modal-manager';
+
+const store = createStore(combineReducers(modalManagerReducers));
+
+class DemoApplication extends React.Component {
+ render() {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+}
+
+export default DemoApplication;
+```
+
+```jsx
+// DemoContainer.jsx
+
+import React, { PropTypes } from 'react';
+import ModalContent, { disclosureName } from './ModalContent';
+
+const DemoContainer = ({ app }) => (
+
+ {
+ this.props.app.disclose({
+ preferredType: 'modal',
+ size: 'small',
+ content: {
+ key: 'DEMO_CONTAINER_MODAL_CONTENT',
+ name: disclosureName,
+ props: {
+ contentText: 'Modal content'
+ },
+ },
+ })
+ }}
+ >
+ Launch Modal
+
+
+)
+
+export default DemoContainer;
+```
+
+```jsx
+// ModalContent.jsx
+
+import AppDelegate from 'terra-clinical-app-delegate';
+
+const ModalContent = ({ app, contentText }) => (
+
+
{contentText}
+
Close Modal
+
+)
+
+export default ModalContent;
+
+const disclosureName = 'ModalContent';
+AppDelegate.registerComponentForDisclosure(disclosureName, ModalContent);
+export { disclosureName };
+```
diff --git a/packages/terra-clinical-modal-manager/lib/ModalManager.js b/packages/terra-clinical-modal-manager/lib/ModalManager.js
new file mode 100644
index 000000000..198f6294f
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/lib/ModalManager.js
@@ -0,0 +1,281 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+var _react = require('react');
+
+var _react2 = _interopRequireDefault(_react);
+
+var _propTypes = require('prop-types');
+
+var _propTypes2 = _interopRequireDefault(_propTypes);
+
+var _classnames = require('classnames');
+
+var _classnames2 = _interopRequireDefault(_classnames);
+
+var _terraModal = require('terra-modal');
+
+var _terraModal2 = _interopRequireDefault(_terraModal);
+
+require('terra-base/lib/baseStyles');
+
+var _terraClinicalAppDelegate = require('terra-clinical-app-delegate');
+
+var _terraClinicalAppDelegate2 = _interopRequireDefault(_terraClinicalAppDelegate);
+
+var _terraClinicalSlideGroup = require('terra-clinical-slide-group');
+
+var _terraClinicalSlideGroup2 = _interopRequireDefault(_terraClinicalSlideGroup);
+
+var _breakpoints = require('terra-responsive-element/lib/breakpoints');
+
+var _breakpoints2 = _interopRequireDefault(_breakpoints);
+
+require('./ModalManager.scss');
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var propTypes = {
+ /**
+ * The AppDelegate instance provided by the containing component. If present, its properties will propagate to the children components.
+ **/
+ app: _terraClinicalAppDelegate2.default.propType,
+
+ /**
+ * Components that will receive the ModalManager's AppDelegate configuration. Components given as children must appropriately handle an `app` prop.
+ **/
+ children: _propTypes2.default.node,
+
+ /**
+ * From `connect`. The Array of component data (key, name, and props) that will be used to instantiate the Modal's inner components.
+ **/
+ modalComponentData: _propTypes2.default.arrayOf(_propTypes2.default.shape({
+ key: _propTypes2.default.string.isRequired,
+ name: _propTypes2.default.string.isRequired,
+ props: _propTypes2.default.object
+ })),
+
+ /**
+ * From `connect`. The desired size of the modal.
+ **/
+ size: _propTypes2.default.oneOf(['tiny', 'small', 'medium', 'large', 'huge']),
+
+ /**
+ * From `connect`. The presentation state of the modal.
+ **/
+ isOpen: _propTypes2.default.bool,
+
+ /**
+ * From `connect`. The maximization state of the modal.
+ **/
+ isMaximized: _propTypes2.default.bool,
+
+ /**
+ * From `connect`. A function that dispatches an `open` action.
+ **/
+ openModal: _propTypes2.default.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `close` action.
+ **/
+ closeModal: _propTypes2.default.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `push` action.
+ **/
+ pushModal: _propTypes2.default.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `pop` action.
+ **/
+ popModal: _propTypes2.default.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `maximize` action.
+ **/
+ maximizeModal: _propTypes2.default.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `minimize` action.
+ **/
+ minimizeModal: _propTypes2.default.func.isRequired
+};
+
+var defaultProps = {
+ isOpen: false,
+ isMaximized: false,
+ size: 'small',
+ modalComponentData: []
+};
+
+var ModalManager = function (_React$Component) {
+ _inherits(ModalManager, _React$Component);
+
+ function ModalManager(props) {
+ _classCallCheck(this, ModalManager);
+
+ // I'm tracking the responsive-fullscreen state outside of React and Redux state to limit the number of
+ // renderings that occur.
+ var _this = _possibleConstructorReturn(this, (ModalManager.__proto__ || Object.getPrototypeOf(ModalManager)).call(this, props));
+
+ _this.forceFullscreenModal = false;
+
+ _this.updateFullscreenState = _this.updateFullscreenState.bind(_this);
+ _this.buildModalComponents = _this.buildModalComponents.bind(_this);
+ return _this;
+ }
+
+ _createClass(ModalManager, [{
+ key: 'componentDidMount',
+ value: function componentDidMount() {
+ this.updateFullscreenState();
+ window.addEventListener('resize', this.updateFullscreenState);
+ }
+ }, {
+ key: 'componentWillUnmount',
+ value: function componentWillUnmount() {
+ window.removeEventListener('resize', this.updateFullscreenState);
+ }
+ }, {
+ key: 'updateFullscreenState',
+ value: function updateFullscreenState() {
+ var previousFullscreenState = this.forceFullscreenModal;
+
+ this.forceFullscreenModal = window.innerWidth < (0, _breakpoints2.default)().small;
+
+ // Only update the modal if it's minimized, open, and changing states.
+ if (!this.props.isMaximized && this.props.isOpen && previousFullscreenState !== this.forceFullscreenModal) {
+ this.forceUpdate();
+ }
+ }
+ }, {
+ key: 'buildModalComponents',
+ value: function buildModalComponents() {
+ var _props = this.props,
+ modalComponentData = _props.modalComponentData,
+ isMaximized = _props.isMaximized,
+ pushModal = _props.pushModal,
+ popModal = _props.popModal,
+ closeModal = _props.closeModal,
+ maximizeModal = _props.maximizeModal,
+ minimizeModal = _props.minimizeModal;
+
+
+ return modalComponentData.map(function (componentData, index) {
+ var ComponentClass = _terraClinicalAppDelegate2.default.getComponentForDisclosure(componentData.name);
+
+ if (!ComponentClass) {
+ return undefined;
+ }
+
+ var appDelegate = _terraClinicalAppDelegate2.default.create({
+ disclose: function disclose(data) {
+ pushModal(data);
+ },
+ dismiss: index > 0 ? function (data) {
+ popModal(data);
+ } : function (data) {
+ closeModal(data);
+ },
+ closeDisclosure: function closeDisclosure(data) {
+ closeModal(data);
+ },
+ goBack: index > 0 ? function (data) {
+ popModal(data);
+ } : null,
+ maximize: !isMaximized ? function (data) {
+ maximizeModal(data);
+ } : null,
+ minimize: isMaximized ? function (data) {
+ minimizeModal(data);
+ } : null
+ });
+
+ return _react2.default.createElement(ComponentClass, _extends({ key: componentData.key }, componentData.props, { app: appDelegate }));
+ });
+ }
+
+ /**
+ * The provided child components are cloned and provided with an AppDelegate instance that contains a new disclose
+ * function that will allow for modal presentation. If an AppDelegate was already provided to the ModalManager through
+ * props, its implementations will be used for APIs not implemented by the ModalManager.
+ */
+
+ }, {
+ key: 'buildChildren',
+ value: function buildChildren() {
+ var _props2 = this.props,
+ app = _props2.app,
+ children = _props2.children,
+ openModal = _props2.openModal;
+
+
+ return _react2.default.Children.map(children, function (child) {
+ var childAppDelegate = _terraClinicalAppDelegate2.default.clone(app, {
+ disclose: function disclose(data) {
+ if (data.preferredType === 'modal' || !app) {
+ openModal(data);
+ } else {
+ app.disclose(data);
+ }
+ }
+ });
+
+ return _react2.default.cloneElement(child, { app: childAppDelegate });
+ });
+ }
+ }, {
+ key: 'render',
+ value: function render() {
+ var _props3 = this.props,
+ closeModal = _props3.closeModal,
+ size = _props3.size,
+ isOpen = _props3.isOpen,
+ isMaximized = _props3.isMaximized;
+
+
+ var modalClassNames = (0, _classnames2.default)(['terraClinical-ModalManager-modal', _defineProperty({}, 'terraClinical-ModalManager-modal--' + size, !(isMaximized || this.forceFullscreenModal))]);
+
+ return _react2.default.createElement(
+ 'div',
+ { className: 'terraClinical-ModalManager' },
+ this.buildChildren(),
+ _react2.default.createElement(
+ _terraModal2.default,
+ {
+ isOpened: isOpen,
+ isFullscreen: isMaximized || this.forceFullscreenModal,
+ classNameModal: modalClassNames,
+ onRequestClose: closeModal,
+ closeOnEsc: true,
+ closeOnOutsideClick: false,
+ ariaLabel: 'Modal'
+ },
+ _react2.default.createElement(_terraClinicalSlideGroup2.default, { items: this.buildModalComponents(), isAnimated: true })
+ )
+ );
+ }
+ }]);
+
+ return ModalManager;
+}(_react2.default.Component);
+
+ModalManager.propTypes = propTypes;
+ModalManager.defaultProps = defaultProps;
+
+exports.default = ModalManager;
\ No newline at end of file
diff --git a/packages/terra-clinical-modal-manager/lib/ModalManager.scss b/packages/terra-clinical-modal-manager/lib/ModalManager.scss
new file mode 100644
index 000000000..04020d5ed
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/lib/ModalManager.scss
@@ -0,0 +1,39 @@
+.terraClinical-ModalManager {
+ height: 100%;
+}
+
+.terraClinical-ModalManager-modal {
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ position: absolute;
+}
+
+// When not fullscreen, set a minimum width so that smaller percentages are still usable in smaller viewports.
+.terraClinical-ModalManager-modal:not(.terra-Modal--fullscreen) { /* stylelint-disable-line selector-class-pattern */
+ min-width: 560px;
+}
+
+.terraClinical-ModalManager-modal--huge {
+ height: 85%;
+ width: 75%;
+}
+
+.terraClinical-ModalManager-modal--large {
+ height: 75%;
+ width: 55%;
+}
+
+.terraClinical-ModalManager-modal--medium {
+ height: 65%;
+ width: 55%;
+}
+
+.terraClinical-ModalManager-modal--small {
+ height: 50%;
+ width: 50%;
+}
+
+.terraClinical-ModalManager-modal--tiny {
+ height: 40%;
+ width: 50%;
+}
diff --git a/packages/terra-clinical-modal-manager/lib/actionTypes.js b/packages/terra-clinical-modal-manager/lib/actionTypes.js
new file mode 100644
index 000000000..6e9c5e7db
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/lib/actionTypes.js
@@ -0,0 +1,11 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+var OPEN = exports.OPEN = 'MODAL_MANAGER_OPEN';
+var CLOSE = exports.CLOSE = 'MODAL_MANAGER_CLOSE';
+var PUSH = exports.PUSH = 'MODAL_MANAGER_PUSH';
+var POP = exports.POP = 'MODAL_MANAGER_POP';
+var MAXIMIZE = exports.MAXIMIZE = 'MODAL_MANAGER_MAXIMIZE';
+var MINIMIZE = exports.MINIMIZE = 'MODAL_MANAGER_MINIMIZE';
\ No newline at end of file
diff --git a/packages/terra-clinical-modal-manager/lib/actions.js b/packages/terra-clinical-modal-manager/lib/actions.js
new file mode 100644
index 000000000..7232f974c
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/lib/actions.js
@@ -0,0 +1,37 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.open = open;
+exports.close = close;
+exports.push = push;
+exports.pop = pop;
+exports.maximize = maximize;
+exports.minimize = minimize;
+
+var _actionTypes = require('./actionTypes');
+
+function open(data) {
+ return { type: _actionTypes.OPEN, data: data };
+}
+
+function close(data) {
+ return { type: _actionTypes.CLOSE, data: data };
+}
+
+function push(data) {
+ return { type: _actionTypes.PUSH, data: data };
+}
+
+function pop(data) {
+ return { type: _actionTypes.POP, data: data };
+}
+
+function maximize(data) {
+ return { type: _actionTypes.MAXIMIZE, data: data };
+}
+
+function minimize(data) {
+ return { type: _actionTypes.MINIMIZE, data: data };
+}
\ No newline at end of file
diff --git a/packages/terra-clinical-modal-manager/lib/index.js b/packages/terra-clinical-modal-manager/lib/index.js
new file mode 100644
index 000000000..fe49d9646
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/lib/index.js
@@ -0,0 +1,69 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+exports.reducers = exports.mapDispatchToProps = exports.mapStateToProps = undefined;
+
+var _reactRedux = require('react-redux');
+
+var _ModalManager = require('./ModalManager');
+
+var _ModalManager2 = _interopRequireDefault(_ModalManager);
+
+var _reducers = require('./reducers');
+
+var _reducers2 = _interopRequireDefault(_reducers);
+
+var _actions = require('./actions');
+
+function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
+
+var mapStateToProps = function mapStateToProps(state) {
+ return function (disclosureState) {
+ return {
+ modalComponentData: disclosureState.componentKeys.map(function (key) {
+ return disclosureState.components[key];
+ }),
+ size: disclosureState.size,
+ isOpen: disclosureState.isOpen,
+ isMaximized: disclosureState.isMaximized
+ };
+ }(state.modalManager);
+};
+
+exports.mapStateToProps = mapStateToProps;
+
+
+var mapDispatchToProps = function mapDispatchToProps(dispatch) {
+ return {
+ openModal: function openModal(data) {
+ dispatch((0, _actions.open)(data));
+ },
+ closeModal: function closeModal(data) {
+ dispatch((0, _actions.close)(data));
+ },
+ pushModal: function pushModal(data) {
+ dispatch((0, _actions.push)(data));
+ },
+ popModal: function popModal(data) {
+ dispatch((0, _actions.pop)(data));
+ },
+ maximizeModal: function maximizeModal(data) {
+ dispatch((0, _actions.maximize)(data));
+ },
+ minimizeModal: function minimizeModal(data) {
+ dispatch((0, _actions.minimize)(data));
+ }
+ };
+};
+
+exports.mapDispatchToProps = mapDispatchToProps;
+exports.default = (0, _reactRedux.connect)(mapStateToProps, mapDispatchToProps)(_ModalManager2.default);
+
+
+var reducers = {
+ modalManager: _reducers2.default
+};
+
+exports.reducers = reducers;
\ No newline at end of file
diff --git a/packages/terra-clinical-modal-manager/lib/reducers.js b/packages/terra-clinical-modal-manager/lib/reducers.js
new file mode 100644
index 000000000..005a99458
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/lib/reducers.js
@@ -0,0 +1,47 @@
+'use strict';
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var _disclosureReducerUtils = require('./shared/disclosureReducerUtils');
+
+var _actionTypes = require('./actionTypes');
+
+var supportedSizes = {
+ tiny: 'tiny',
+ small: 'small',
+ medium: 'medium',
+ large: 'large',
+ huge: 'huge'
+};
+
+var defaultModalState = _extends({}, _disclosureReducerUtils.defaultState, {
+ size: supportedSizes.small
+});
+
+var modalManager = function modalManager() {
+ var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : defaultModalState;
+ var action = arguments[1];
+
+ switch (action.type) {
+ case _actionTypes.OPEN:
+ return _extends({}, (0, _disclosureReducerUtils.open)(state, action), { size: action.data.size || supportedSizes.small });
+ case _actionTypes.CLOSE:
+ return defaultModalState;
+ case _actionTypes.PUSH:
+ return (0, _disclosureReducerUtils.push)(state, action);
+ case _actionTypes.POP:
+ return (0, _disclosureReducerUtils.pop)(state, action);
+ case _actionTypes.MAXIMIZE:
+ return (0, _disclosureReducerUtils.maximize)(state, action);
+ case _actionTypes.MINIMIZE:
+ return (0, _disclosureReducerUtils.minimize)(state, action);
+ default:
+ return state;
+ }
+};
+
+exports.default = modalManager;
\ No newline at end of file
diff --git a/packages/terra-clinical-modal-manager/lib/shared/disclosureReducerUtils.js b/packages/terra-clinical-modal-manager/lib/shared/disclosureReducerUtils.js
new file mode 100644
index 000000000..cc88dff2c
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/lib/shared/disclosureReducerUtils.js
@@ -0,0 +1,85 @@
+"use strict";
+
+Object.defineProperty(exports, "__esModule", {
+ value: true
+});
+
+var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
+
+var cloneDisclosureState = function cloneDisclosureState(state) {
+ var newState = _extends({}, state);
+ newState.componentKeys = _extends([], newState.componentKeys);
+ newState.components = _extends({}, newState.components);
+
+ return newState;
+};
+
+var defaultState = Object.freeze({
+ isOpen: false,
+ isMaximized: false,
+ componentKeys: [],
+ components: {}
+});
+
+var open = function open(state, action) {
+ var newState = cloneDisclosureState(state);
+
+ newState.isOpen = true;
+ newState.componentKeys = [action.data.content.key];
+ newState.components[action.data.content.key] = {
+ name: action.data.content.name,
+ props: action.data.content.props,
+ key: action.data.content.key
+ };
+
+ return newState;
+};
+
+var close = function close() {
+ return defaultState;
+};
+
+var push = function push(state, action) {
+ var newState = cloneDisclosureState(state);
+
+ newState.componentKeys.push(action.data.content.key);
+ newState.components[action.data.content.key] = {
+ name: action.data.content.name,
+ props: action.data.content.props,
+ key: action.data.content.key
+ };
+
+ return newState;
+};
+
+var pop = function pop(state) {
+ var newState = cloneDisclosureState(state);
+
+ newState.components[newState.componentKeys.pop()] = undefined;
+
+ return newState;
+};
+
+var maximize = function maximize(state) {
+ var newState = cloneDisclosureState(state);
+
+ newState.isMaximized = true;
+
+ return newState;
+};
+
+var minimize = function minimize(state) {
+ var newState = cloneDisclosureState(state);
+
+ newState.isMaximized = false;
+
+ return newState;
+};
+
+exports.defaultState = defaultState;
+exports.open = open;
+exports.close = close;
+exports.push = push;
+exports.pop = pop;
+exports.maximize = maximize;
+exports.minimize = minimize;
\ No newline at end of file
diff --git a/packages/terra-clinical-modal-manager/package.json b/packages/terra-clinical-modal-manager/package.json
new file mode 100644
index 000000000..59ce14b48
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/package.json
@@ -0,0 +1,66 @@
+{
+ "name": "terra-clinical-modal-manager",
+ "main": "lib/index.js",
+ "private": true,
+ "version": "0.0.0",
+ "description": "A Redux-backed Container component that dynamically presents components in a Terra Modal",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/cerner/terra-clinical.git"
+ },
+ "keywords": [
+ "Cerner",
+ "Terra",
+ "Clinical",
+ "terra-clinical-modal-manager",
+ "ModalManager",
+ "UI"
+ ],
+ "author": "Cerner Corporation",
+ "license": "Apache-2.0",
+ "bugs": {
+ "url": "https://github.com/cerner/terra-clinical/issues"
+ },
+ "homepage": "https://github.com/cerner/terra-clinical#readme",
+ "devDependencies": {
+ "terra-toolkit": "^0.x",
+ "terra-clinical-app-delegate": "^0.x",
+ "react-redux": "^5.0.4",
+ "redux": "^3.6.0"
+ },
+ "peerDependencies": {
+ "react": "^15.4.2",
+ "react-dom": "^15.4.2",
+ "react-redux": "^5.0.4",
+ "redux": "^3.6.0",
+ "terra-base": "^0.x",
+ "terra-mixins": "^1.0.0",
+ "terra-clinical-app-delegate": "^0.x"
+ },
+ "dependencies": {
+ "classnames": "^2.2.5",
+ "prop-types": "^15.5.8",
+ "terra-base": "^0.x",
+ "terra-mixins": "^1.0.0",
+ "terra-modal": "0.1.1",
+ "terra-responsive-element": "^0.x",
+ "terra-clinical-slide-group": "^0.x"
+ },
+ "scripts": {
+ "compile": "npm run compile:clean && npm run compile:build",
+ "compile:clean": "$(cd ..; npm bin)/rimraf lib",
+ "compile:build": "$(cd ..; npm bin)/babel src --out-dir lib --copy-files",
+ "lint": "npm run lint:js && npm run lint:scss",
+ "lint:js": "$(cd ..; npm bin)/eslint --ext .js,.jsx . --ignore-path ../../.eslintignore",
+ "lint:scss": "$(cd ..; npm bin)/stylelint src/**/*.scss",
+ "test": "npm run test:spec && npm run test:nightwatch-default",
+ "test:spec": "$(cd ..; npm bin)/jest --config ../../jestconfig.json",
+ "test:all": "npm run test:nightwatch-default && npm run test:nightwatch-chrome && npm run test:nightwatch-firefox && npm run test:nightwatch-safari",
+ "test:nightwatch-default": "WEBPACK_CONFIG_PATH=../../../../terra-clinical-site/webpack.config SPECTRE_TEST_SUITE=terra-clinical-modal-manager node ./node_modules/terra-toolkit/lib/scripts/nightwatch.js",
+ "test:nightwatch-chrome": "WEBPACK_CONFIG_PATH=../../../../terra-clinical-site/webpack.config SPECTRE_TEST_SUITE=terra-clinical-modal-manager node ./node_modules/terra-toolkit/lib/scripts/nightwatch.js chrome",
+ "test:nightwatch-firefox": "WEBPACK_CONFIG_PATH=../../../../terra-clinical-site/webpack.config SPECTRE_TEST_SUITE=terra-clinical-modal-manager node ./node_modules/terra-toolkit/lib/scripts/nightwatch.js firefox",
+ "test:nightwatch-safari": "WEBPACK_CONFIG_PATH=../../../../terra-clinical-site/webpack.config SPECTRE_TEST_SUITE=terra-clinical-modal-manager node ./node_modules/terra-toolkit/lib/scripts/nightwatch-non-parallel.js safari",
+ "test:remote": "WEBPACK_CONFIG_PATH=../../../../terra-clinical-site/webpack.config REMOTE=true node ./node_modules/terra-toolkit/lib/scripts/nightwatch-process.js --config tests/nightwatch.conf.js",
+ "test:remote:all": "WEBPACK_CONFIG_PATH=../../../../terra-clinical-site/webpack.config REMOTE=true node ./node_modules/terra-toolkit/lib/scripts/nightwatch-process.js --config tests/nightwatch.conf.js --env chrome-tiny,chrome-small,chrome-medium,chrome-large,chrome-huge,chrome-enormous,firefox-tiny,firefox-small,firefox-medium,firefox-large,firefox-huge,firefox-enormous,ie10-tiny,ie10-small,ie10-medium,ie10-large,ie10-huge,ie10-enormous,ie11-tiny,ie11-small,ie11-medium,ie11-large,ie11-huge,ie11-enormous,edge-tiny,edge-small,edge-medium,edge-large,edge-huge,edge-enormous,safari-tiny,safari-small,safari-medium,safari-large,safari-huge,safari-enormous"
+ }
+}
diff --git a/packages/terra-clinical-modal-manager/src/ModalManager.jsx b/packages/terra-clinical-modal-manager/src/ModalManager.jsx
new file mode 100644
index 000000000..f71fa7b71
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/src/ModalManager.jsx
@@ -0,0 +1,203 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import Modal from 'terra-modal';
+import 'terra-base/lib/baseStyles';
+
+import AppDelegate from 'terra-clinical-app-delegate';
+import SlideGroup from 'terra-clinical-slide-group';
+import getBreakpoints from 'terra-responsive-element/lib/breakpoints';
+
+import './ModalManager.scss';
+
+const propTypes = {
+ /**
+ * The AppDelegate instance provided by the containing component. If present, its properties will propagate to the children components.
+ **/
+ app: AppDelegate.propType,
+
+ /**
+ * Components that will receive the ModalManager's AppDelegate configuration. Components given as children must appropriately handle an `app` prop.
+ **/
+ children: PropTypes.node,
+
+ /**
+ * From `connect`. The Array of component data (key, name, and props) that will be used to instantiate the Modal's inner components.
+ **/
+ modalComponentData: PropTypes.arrayOf(PropTypes.shape({
+ key: PropTypes.string.isRequired,
+ name: PropTypes.string.isRequired,
+ props: PropTypes.object,
+ })),
+
+ /**
+ * From `connect`. The desired size of the modal.
+ **/
+ size: PropTypes.oneOf(['tiny', 'small', 'medium', 'large', 'huge']),
+
+ /**
+ * From `connect`. The presentation state of the modal.
+ **/
+ isOpen: PropTypes.bool,
+
+ /**
+ * From `connect`. The maximization state of the modal.
+ **/
+ isMaximized: PropTypes.bool,
+
+ /**
+ * From `connect`. A function that dispatches an `open` action.
+ **/
+ openModal: PropTypes.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `close` action.
+ **/
+ closeModal: PropTypes.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `push` action.
+ **/
+ pushModal: PropTypes.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `pop` action.
+ **/
+ popModal: PropTypes.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `maximize` action.
+ **/
+ maximizeModal: PropTypes.func.isRequired,
+
+ /**
+ * From `connect`. A function that dispatches a `minimize` action.
+ **/
+ minimizeModal: PropTypes.func.isRequired,
+};
+
+const defaultProps = {
+ isOpen: false,
+ isMaximized: false,
+ size: 'small',
+ modalComponentData: [],
+};
+
+class ModalManager extends React.Component {
+ constructor(props) {
+ super(props);
+
+ // I'm tracking the responsive-fullscreen state outside of React and Redux state to limit the number of
+ // renderings that occur.
+ this.forceFullscreenModal = false;
+
+ this.updateFullscreenState = this.updateFullscreenState.bind(this);
+ this.buildModalComponents = this.buildModalComponents.bind(this);
+ }
+
+ componentDidMount() {
+ this.updateFullscreenState();
+ window.addEventListener('resize', this.updateFullscreenState);
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.updateFullscreenState);
+ }
+
+ updateFullscreenState() {
+ const previousFullscreenState = this.forceFullscreenModal;
+
+ this.forceFullscreenModal = window.innerWidth < getBreakpoints().small;
+
+ // Only update the modal if it's minimized, open, and changing states.
+ if (!this.props.isMaximized && this.props.isOpen && previousFullscreenState !== this.forceFullscreenModal) {
+ this.forceUpdate();
+ }
+ }
+
+ buildModalComponents() {
+ const { modalComponentData, isMaximized, pushModal, popModal, closeModal, maximizeModal, minimizeModal } = this.props;
+
+ return modalComponentData.map((componentData, index) => {
+ const ComponentClass = AppDelegate.getComponentForDisclosure(componentData.name);
+
+ if (!ComponentClass) {
+ return undefined;
+ }
+
+ const appDelegate = AppDelegate.create({
+ disclose: (data) => {
+ pushModal(data);
+ },
+ dismiss: (index > 0 ?
+ (data) => {
+ popModal(data);
+ } :
+ (data) => {
+ closeModal(data);
+ }
+ ),
+ closeDisclosure: (data) => { closeModal(data); },
+ goBack: index > 0 ? (data) => { popModal(data); } : null,
+ maximize: !isMaximized ? (data) => { maximizeModal(data); } : null,
+ minimize: isMaximized ? (data) => { minimizeModal(data); } : null,
+ });
+
+ return ;
+ });
+ }
+
+ /**
+ * The provided child components are cloned and provided with an AppDelegate instance that contains a new disclose
+ * function that will allow for modal presentation. If an AppDelegate was already provided to the ModalManager through
+ * props, its implementations will be used for APIs not implemented by the ModalManager.
+ */
+ buildChildren() {
+ const { app, children, openModal } = this.props;
+
+ return React.Children.map(children, (child) => {
+ const childAppDelegate = AppDelegate.clone(app, {
+ disclose: (data) => {
+ if (data.preferredType === 'modal' || !app) {
+ openModal(data);
+ } else {
+ app.disclose(data);
+ }
+ },
+ });
+
+ return React.cloneElement(child, { app: childAppDelegate });
+ });
+ }
+
+ render() {
+ const { closeModal, size, isOpen, isMaximized } = this.props;
+
+ const modalClassNames = classNames([
+ 'terraClinical-ModalManager-modal',
+ { [`terraClinical-ModalManager-modal--${size}`]: !(isMaximized || this.forceFullscreenModal) },
+ ]);
+
+ return (
+
+ {this.buildChildren()}
+
+
+
+
+ );
+ }
+}
+
+ModalManager.propTypes = propTypes;
+ModalManager.defaultProps = defaultProps;
+
+export default ModalManager;
diff --git a/packages/terra-clinical-modal-manager/src/ModalManager.scss b/packages/terra-clinical-modal-manager/src/ModalManager.scss
new file mode 100644
index 000000000..04020d5ed
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/src/ModalManager.scss
@@ -0,0 +1,39 @@
+.terraClinical-ModalManager {
+ height: 100%;
+}
+
+.terraClinical-ModalManager-modal {
+ overflow: auto;
+ -webkit-overflow-scrolling: touch;
+ position: absolute;
+}
+
+// When not fullscreen, set a minimum width so that smaller percentages are still usable in smaller viewports.
+.terraClinical-ModalManager-modal:not(.terra-Modal--fullscreen) { /* stylelint-disable-line selector-class-pattern */
+ min-width: 560px;
+}
+
+.terraClinical-ModalManager-modal--huge {
+ height: 85%;
+ width: 75%;
+}
+
+.terraClinical-ModalManager-modal--large {
+ height: 75%;
+ width: 55%;
+}
+
+.terraClinical-ModalManager-modal--medium {
+ height: 65%;
+ width: 55%;
+}
+
+.terraClinical-ModalManager-modal--small {
+ height: 50%;
+ width: 50%;
+}
+
+.terraClinical-ModalManager-modal--tiny {
+ height: 40%;
+ width: 50%;
+}
diff --git a/packages/terra-clinical-modal-manager/src/actionTypes.js b/packages/terra-clinical-modal-manager/src/actionTypes.js
new file mode 100644
index 000000000..ffde6555d
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/src/actionTypes.js
@@ -0,0 +1,6 @@
+export const OPEN = 'MODAL_MANAGER_OPEN';
+export const CLOSE = 'MODAL_MANAGER_CLOSE';
+export const PUSH = 'MODAL_MANAGER_PUSH';
+export const POP = 'MODAL_MANAGER_POP';
+export const MAXIMIZE = 'MODAL_MANAGER_MAXIMIZE';
+export const MINIMIZE = 'MODAL_MANAGER_MINIMIZE';
diff --git a/packages/terra-clinical-modal-manager/src/actions.js b/packages/terra-clinical-modal-manager/src/actions.js
new file mode 100644
index 000000000..107150ddd
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/src/actions.js
@@ -0,0 +1,32 @@
+import {
+ OPEN,
+ CLOSE,
+ PUSH,
+ POP,
+ MAXIMIZE,
+ MINIMIZE,
+} from './actionTypes';
+
+export function open(data) {
+ return { type: OPEN, data };
+}
+
+export function close(data) {
+ return { type: CLOSE, data };
+}
+
+export function push(data) {
+ return { type: PUSH, data };
+}
+
+export function pop(data) {
+ return { type: POP, data };
+}
+
+export function maximize(data) {
+ return { type: MAXIMIZE, data };
+}
+
+export function minimize(data) {
+ return { type: MINIMIZE, data };
+}
diff --git a/packages/terra-clinical-modal-manager/src/index.jsx b/packages/terra-clinical-modal-manager/src/index.jsx
new file mode 100644
index 000000000..d413b079a
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/src/index.jsx
@@ -0,0 +1,36 @@
+import { connect } from 'react-redux';
+
+import ModalManager from './ModalManager';
+
+import modalManagerReducers from './reducers';
+import { open, close, push, pop, maximize, minimize } from './actions';
+
+const mapStateToProps = state => (
+ (disclosureState => ({
+ modalComponentData: disclosureState.componentKeys.map(key => (disclosureState.components[key])),
+ size: disclosureState.size,
+ isOpen: disclosureState.isOpen,
+ isMaximized: disclosureState.isMaximized,
+ }))(state.modalManager)
+);
+
+export { mapStateToProps };
+
+const mapDispatchToProps = dispatch => ({
+ openModal: (data) => { dispatch(open(data)); },
+ closeModal: (data) => { dispatch(close(data)); },
+ pushModal: (data) => { dispatch(push(data)); },
+ popModal: (data) => { dispatch(pop(data)); },
+ maximizeModal: (data) => { dispatch(maximize(data)); },
+ minimizeModal: (data) => { dispatch(minimize(data)); },
+});
+
+export { mapDispatchToProps };
+
+export default connect(mapStateToProps, mapDispatchToProps)(ModalManager);
+
+const reducers = {
+ modalManager: modalManagerReducers,
+};
+
+export { reducers };
diff --git a/packages/terra-clinical-modal-manager/src/reducers.js b/packages/terra-clinical-modal-manager/src/reducers.js
new file mode 100644
index 000000000..fda7ea0e4
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/src/reducers.js
@@ -0,0 +1,43 @@
+import { open, push, pop, maximize, minimize, defaultState } from './shared/disclosureReducerUtils';
+
+import {
+ OPEN,
+ CLOSE,
+ PUSH,
+ POP,
+ MAXIMIZE,
+ MINIMIZE,
+} from './actionTypes';
+
+const supportedSizes = {
+ tiny: 'tiny',
+ small: 'small',
+ medium: 'medium',
+ large: 'large',
+ huge: 'huge',
+};
+
+const defaultModalState = Object.assign({}, defaultState, {
+ size: supportedSizes.small,
+});
+
+const modalManager = (state = defaultModalState, action) => {
+ switch (action.type) {
+ case OPEN:
+ return Object.assign({}, open(state, action), { size: action.data.size || supportedSizes.small });
+ case CLOSE:
+ return defaultModalState;
+ case PUSH:
+ return push(state, action);
+ case POP:
+ return pop(state, action);
+ case MAXIMIZE:
+ return maximize(state, action);
+ case MINIMIZE:
+ return minimize(state, action);
+ default:
+ return state;
+ }
+};
+
+export default modalManager;
diff --git a/packages/terra-clinical-modal-manager/src/shared/disclosureReducerUtils.js b/packages/terra-clinical-modal-manager/src/shared/disclosureReducerUtils.js
new file mode 100644
index 000000000..32c443320
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/src/shared/disclosureReducerUtils.js
@@ -0,0 +1,69 @@
+const cloneDisclosureState = (state) => {
+ const newState = Object.assign({}, state);
+ newState.componentKeys = Object.assign([], newState.componentKeys);
+ newState.components = Object.assign({}, newState.components);
+
+ return newState;
+};
+
+const defaultState = Object.freeze({
+ isOpen: false,
+ isMaximized: false,
+ componentKeys: [],
+ components: {},
+});
+
+const open = (state, action) => {
+ const newState = cloneDisclosureState(state);
+
+ newState.isOpen = true;
+ newState.componentKeys = [action.data.content.key];
+ newState.components[action.data.content.key] = {
+ name: action.data.content.name,
+ props: action.data.content.props,
+ key: action.data.content.key,
+ };
+
+ return newState;
+};
+
+const close = () => (defaultState);
+
+const push = (state, action) => {
+ const newState = cloneDisclosureState(state);
+
+ newState.componentKeys.push(action.data.content.key);
+ newState.components[action.data.content.key] = {
+ name: action.data.content.name,
+ props: action.data.content.props,
+ key: action.data.content.key,
+ };
+
+ return newState;
+};
+
+const pop = (state) => {
+ const newState = cloneDisclosureState(state);
+
+ newState.components[newState.componentKeys.pop()] = undefined;
+
+ return newState;
+};
+
+const maximize = (state) => {
+ const newState = cloneDisclosureState(state);
+
+ newState.isMaximized = true;
+
+ return newState;
+};
+
+const minimize = (state) => {
+ const newState = cloneDisclosureState(state);
+
+ newState.isMaximized = false;
+
+ return newState;
+};
+
+export { defaultState, open, close, push, pop, maximize, minimize };
diff --git a/packages/terra-clinical-modal-manager/tests/jest/ModalManager.test.jsx b/packages/terra-clinical-modal-manager/tests/jest/ModalManager.test.jsx
new file mode 100644
index 000000000..7517e0d73
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/jest/ModalManager.test.jsx
@@ -0,0 +1,251 @@
+import React from 'react';
+import AppDelegate from 'terra-clinical-app-delegate';
+import ModalManager from '../../src/ModalManager';
+
+const TestContainer = () => (
+ Hello World
+);
+
+const openModal = () => {};
+const closeModal = () => {};
+const pushModal = () => {};
+const popModal = () => {};
+const maximizeModal = () => {};
+const minimizeModal = () => {};
+
+const requiredProps = { openModal, closeModal, pushModal, popModal, maximizeModal, minimizeModal };
+
+describe('ModalManger', () => {
+ it('should render the ModalManager with defaults', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+
+ describe('- sizes -', () => {
+ it('should render the ModalManager with tiny size', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should render the ModalManager with small size', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should render the ModalManager with medium size', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should render the ModalManager with large size', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should render the ModalManager with huge size', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+ });
+
+ describe('- isOpen -', () => {
+ it('should render the ModalManager as open', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+ });
+
+ describe('- isMaximized -', () => {
+ it('should render the ModalManager as maximized', () => {
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+ });
+
+ describe('- app -', () => {
+ it('should render the ModalManager with given AppDelegate as source', () => {
+ const modalManager = (
+ {},
+ dismiss: () => {},
+ })}
+ {...requiredProps}
+ >
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+ });
+
+ describe('- modal components -', () => {
+ it('should render the ModalManager with given modal contents', () => {
+ AppDelegate.getComponentForDisclosure = jest.fn()
+ .mockReturnValueOnce(TestContainer)
+ .mockReturnValueOnce(TestContainer);
+
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should render the ModalManager with given modal contents when maximized', () => {
+ AppDelegate.getComponentForDisclosure = jest.fn()
+ .mockReturnValueOnce(TestContainer)
+ .mockReturnValueOnce(TestContainer);
+
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+
+ it('should render the ModalManager with given modal contents when components cannot be retrieved', () => {
+ AppDelegate.getComponentForDisclosure = jest.fn()
+ .mockReturnValueOnce(undefined)
+ .mockReturnValueOnce(undefined);
+
+ const modalManager = (
+
+
+
+ );
+
+ const result = shallow(modalManager);
+ expect(result).toMatchSnapshot();
+ });
+ });
+});
diff --git a/packages/terra-clinical-modal-manager/tests/jest/__snapshots__/ModalManager.test.jsx.snap b/packages/terra-clinical-modal-manager/tests/jest/__snapshots__/ModalManager.test.jsx.snap
new file mode 100644
index 000000000..c85e0e47b
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/jest/__snapshots__/ModalManager.test.jsx.snap
@@ -0,0 +1,377 @@
+exports[`ModalManger - app - should render the ModalManager with given AppDelegate as source 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - isMaximized - should render the ModalManager as maximized 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - isOpen - should render the ModalManager as open 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - modal components - should render the ModalManager with given modal contents 1`] = `
+
+
+
+ ,
+ ,
+ ]
+ } />
+
+
+`;
+
+exports[`ModalManger - modal components - should render the ModalManager with given modal contents when components cannot be retrieved 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - modal components - should render the ModalManager with given modal contents when maximized 1`] = `
+
+
+
+ ,
+ ,
+ ]
+ } />
+
+
+`;
+
+exports[`ModalManger - sizes - should render the ModalManager with huge size 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - sizes - should render the ModalManager with large size 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - sizes - should render the ModalManager with medium size 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - sizes - should render the ModalManager with small size 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger - sizes - should render the ModalManager with tiny size 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`ModalManger should render the ModalManager with defaults 1`] = `
+
+
+
+
+
+
+`;
diff --git a/packages/terra-clinical-modal-manager/tests/jest/actions.test.jsx b/packages/terra-clinical-modal-manager/tests/jest/actions.test.jsx
new file mode 100644
index 000000000..9b407fd25
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/jest/actions.test.jsx
@@ -0,0 +1,63 @@
+import {
+ OPEN,
+ CLOSE,
+ PUSH,
+ POP,
+ MAXIMIZE,
+ MINIMIZE,
+} from '../../src/actionTypes';
+
+import {
+ open,
+ close,
+ push,
+ pop,
+ maximize,
+ minimize,
+} from '../../src/actions';
+
+const actionData = { data: 'wooo' };
+
+describe('modalManager actions', () => {
+ it('should create an action to OPEN the modal manager', () => {
+ expect(open(actionData)).toEqual({
+ type: OPEN,
+ data: actionData,
+ });
+ });
+
+ it('should create an action to CLOSE the modal manager', () => {
+ expect(close(actionData)).toEqual({
+ type: CLOSE,
+ data: actionData,
+ });
+ });
+
+ it('should create an action to PUSH content on the modal manager', () => {
+ expect(push(actionData)).toEqual({
+ type: PUSH,
+ data: actionData,
+ });
+ });
+
+ it('should create an action to PUSH content off the modal manager', () => {
+ expect(pop(actionData)).toEqual({
+ type: POP,
+ data: actionData,
+ });
+ });
+
+ it('should create an action to MAXIMIZE the modal manager', () => {
+ expect(maximize(actionData)).toEqual({
+ type: MAXIMIZE,
+ data: actionData,
+ });
+ });
+
+ it('should create an action to MINIMIZE the modal manager', () => {
+ expect(minimize(actionData)).toEqual({
+ type: MINIMIZE,
+ data: actionData,
+ });
+ });
+});
diff --git a/packages/terra-clinical-modal-manager/tests/jest/index.test.js b/packages/terra-clinical-modal-manager/tests/jest/index.test.js
new file mode 100644
index 000000000..9c0b5ff25
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/jest/index.test.js
@@ -0,0 +1,163 @@
+import { mapStateToProps, mapDispatchToProps } from '../../src/index';
+
+import {
+ OPEN,
+ CLOSE,
+ PUSH,
+ POP,
+ MAXIMIZE,
+ MINIMIZE,
+} from '../../src/actionTypes';
+
+describe('mapStateToProps', () => {
+ it('should properly map the modal manager state to props', () => {
+ const componentData = [{
+ key: '1',
+ name: 'COMP1',
+ props: { value: 1 },
+ }, {
+ key: '2',
+ name: 'COMP2',
+ props: { value: 2 },
+ }, {
+ key: '3',
+ name: 'COMP3',
+ props: { value: 3 },
+ }];
+
+ // The values used here are kind of junky and don't match the state schema, but I'm doing that on purpose to
+ // validate that the values are passed through from state directly and not manipulated in any way.
+ const state = {
+ modalManager: {
+ componentKeys: ['1', '2', '3'],
+ components: {
+ 1: componentData[0],
+ 2: componentData[1],
+ 3: componentData[2],
+ },
+ isOpen: 'IS_OPEN_VALUE',
+ isMaximized: 'IS_MAXIMIZED_VALUE',
+ size: 'SIZE_VALUE',
+ },
+ };
+
+ const expectedResult = {
+ modalComponentData: componentData,
+ isOpen: 'IS_OPEN_VALUE',
+ isMaximized: 'IS_MAXIMIZED_VALUE',
+ size: 'SIZE_VALUE',
+ };
+
+ expect(mapStateToProps(state)).toEqual(expectedResult);
+ });
+
+ it('should properly map the modal manager state to props when no component data is present', () => {
+ const componentData = [];
+
+ const state = {
+ modalManager: {
+ componentKeys: [],
+ components: {},
+ isOpen: 'IS_OPEN_VALUE',
+ isMaximized: 'IS_MAXIMIZED_VALUE',
+ size: 'SIZE_VALUE',
+ },
+ };
+
+ const expectedResult = {
+ modalComponentData: componentData,
+ isOpen: 'IS_OPEN_VALUE',
+ isMaximized: 'IS_MAXIMIZED_VALUE',
+ size: 'SIZE_VALUE',
+ };
+
+ expect(mapStateToProps(state)).toEqual(expectedResult);
+ });
+});
+
+describe('mapDispatchToProps', () => {
+ it('should properly map the dispatch state to props', () => {
+ const testDispatch = jest.fn();
+
+ const result = mapDispatchToProps(testDispatch);
+
+ expect(result.openModal).toBeDefined();
+ expect(result.closeModal).toBeDefined();
+ expect(result.pushModal).toBeDefined();
+ expect(result.popModal).toBeDefined();
+ expect(result.maximizeModal).toBeDefined();
+ expect(result.minimizeModal).toBeDefined();
+ });
+
+ it('should properly setup openModal function', () => {
+ const testDispatch = jest.fn();
+
+ const result = mapDispatchToProps(testDispatch);
+
+ expect(result.openModal).toBeDefined();
+
+ result.openModal({ test: 'data' });
+
+ expect(testDispatch).toHaveBeenCalledWith({ type: OPEN, data: { test: 'data' } });
+ });
+
+ it('should properly setup closeModal function', () => {
+ const testDispatch = jest.fn();
+
+ const result = mapDispatchToProps(testDispatch);
+
+ expect(result.closeModal).toBeDefined();
+
+ result.closeModal({ test: 'data' });
+
+ expect(testDispatch).toHaveBeenCalledWith({ type: CLOSE, data: { test: 'data' } });
+ });
+
+ it('should properly setup pushModal function', () => {
+ const testDispatch = jest.fn();
+
+ const result = mapDispatchToProps(testDispatch);
+
+ expect(result.pushModal).toBeDefined();
+
+ result.pushModal({ test: 'data' });
+
+ expect(testDispatch).toHaveBeenCalledWith({ type: PUSH, data: { test: 'data' } });
+ });
+
+ it('should properly setup popModal function', () => {
+ const testDispatch = jest.fn();
+
+ const result = mapDispatchToProps(testDispatch);
+
+ expect(result.popModal).toBeDefined();
+
+ result.popModal({ test: 'data' });
+
+ expect(testDispatch).toHaveBeenCalledWith({ type: POP, data: { test: 'data' } });
+ });
+
+ it('should properly setup maximizeModal function', () => {
+ const testDispatch = jest.fn();
+
+ const result = mapDispatchToProps(testDispatch);
+
+ expect(result.maximizeModal).toBeDefined();
+
+ result.maximizeModal({ test: 'data' });
+
+ expect(testDispatch).toHaveBeenCalledWith({ type: MAXIMIZE, data: { test: 'data' } });
+ });
+
+ it('should properly setup minimizeModal function', () => {
+ const testDispatch = jest.fn();
+
+ const result = mapDispatchToProps(testDispatch);
+
+ expect(result.minimizeModal).toBeDefined();
+
+ result.minimizeModal({ test: 'data' });
+
+ expect(testDispatch).toHaveBeenCalledWith({ type: MINIMIZE, data: { test: 'data' } });
+ });
+});
diff --git a/packages/terra-clinical-modal-manager/tests/jest/reducers.test.jsx b/packages/terra-clinical-modal-manager/tests/jest/reducers.test.jsx
new file mode 100644
index 000000000..f667198bd
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/jest/reducers.test.jsx
@@ -0,0 +1,259 @@
+import modalManager from '../../src/reducers';
+import {
+ OPEN,
+ CLOSE,
+ PUSH,
+ POP,
+ MAXIMIZE,
+ MINIMIZE,
+} from '../../src/actionTypes';
+
+describe('modalManager reducer', () => {
+ it('should return the given state when action type is not recognized', () => {
+ const initialState = { state: 'initial' };
+
+ const result = modalManager(initialState, { type: 'TEST_NOT_SUPPORTED_ACTION' });
+
+ expect(result).toBe(initialState);
+ });
+
+ it('should return the default state when the initial state is undefined', () => {
+ const result = modalManager(undefined, { type: 'TEST_NOT_SUPPORTED_ACTION' });
+
+ expect(result.componentKeys).toEqual([]);
+ expect(result.components).toEqual({});
+ expect(result.isMaximized).toEqual(false);
+ expect(result.isOpen).toEqual(false);
+ expect(result.size).toEqual('small');
+ });
+
+ it('should return state after open action', () => {
+ const content = {
+ key: 'test_key',
+ name: 'TestComponent',
+ props: { prop1: 'test', prop2: 'test' },
+ };
+
+ const initialState = modalManager(undefined, {});
+ const result = modalManager(initialState, {
+ type: OPEN,
+ data: {
+ preferredType: 'modal',
+ size: 'tiny',
+ content,
+ },
+ });
+
+ const expected = {
+ componentKeys: [content.key],
+ components: {
+ [`${content.key}`]: {
+ name: content.name,
+ props: content.props,
+ key: `${content.key}`,
+ },
+ },
+ isMaximized: false,
+ isOpen: true,
+ size: 'tiny',
+ };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return state after open action with default size', () => {
+ const content = {
+ key: 'test_key',
+ name: 'TestComponent',
+ props: { prop1: 'test', prop2: 'test' },
+ };
+
+ const initialState = modalManager(undefined, {});
+ const result = modalManager(initialState, {
+ type: OPEN,
+ data: {
+ preferredType: 'modal',
+ content,
+ },
+ });
+
+ const expected = {
+ componentKeys: [content.key],
+ components: {
+ [`${content.key}`]: {
+ name: content.name,
+ props: content.props,
+ key: `${content.key}`,
+ },
+ },
+ isMaximized: false,
+ isOpen: true,
+ size: 'small',
+ };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return state after close action', () => {
+ const initialState = modalManager(undefined, {});
+ const result = modalManager(initialState, {
+ type: CLOSE,
+ data: {},
+ });
+
+ expect(result).toEqual(initialState);
+ });
+
+ it('should return state after push action', () => {
+ const content = {
+ key: 'test_key',
+ name: 'TestComponent',
+ props: { prop1: 'test', prop2: 'test' },
+ };
+
+ const initialState = {
+ componentKeys: ['COMPONENT_1', 'COMPONENT_2'],
+ components: {
+ COMPONENT_1: {
+ name: 'Component1',
+ props: {},
+ key: 'COMPONENT_1',
+ },
+ COMPONENT_2: {
+ name: 'Component2',
+ props: {},
+ key: 'COMPONENT_2',
+ },
+ },
+ isOpen: true,
+ isMaximized: false,
+ size: 'large',
+ };
+
+ const result = modalManager(initialState, {
+ type: PUSH,
+ data: {
+ preferredType: 'modal',
+ content,
+ },
+ });
+
+ const expected = {
+ componentKeys: [initialState.componentKeys[0], initialState.componentKeys[1], content.key],
+ components: Object.assign({}, initialState.components, {
+ [`${content.key}`]: {
+ name: content.name,
+ props: content.props,
+ key: `${content.key}`,
+ },
+ }),
+ isMaximized: false,
+ isOpen: true,
+ size: 'large',
+ };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return state after pop action', () => {
+ const initialState = {
+ componentKeys: ['COMPONENT_1', 'COMPONENT_2'],
+ components: {
+ COMPONENT_1: {
+ name: 'Component1',
+ props: {},
+ key: 'COMPONENT_1',
+ },
+ COMPONENT_2: {
+ name: 'Component2',
+ props: {},
+ key: 'COMPONENT_2',
+ },
+ },
+ isOpen: true,
+ isMaximized: false,
+ size: 'large',
+ };
+
+ const result = modalManager(initialState, {
+ type: POP,
+ data: {},
+ });
+
+ const expected = {
+ componentKeys: [initialState.componentKeys[0]],
+ components: {
+ COMPONENT_1: {
+ name: 'Component1',
+ props: {},
+ key: 'COMPONENT_1',
+ },
+ },
+ isMaximized: false,
+ isOpen: true,
+ size: 'large',
+ };
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return state after maximize action', () => {
+ const initialState = {
+ componentKeys: ['COMPONENT_1', 'COMPONENT_2'],
+ components: {
+ COMPONENT_1: {
+ name: 'Component1',
+ props: {},
+ key: 'COMPONENT_1',
+ },
+ COMPONENT_2: {
+ name: 'Component2',
+ props: {},
+ key: 'COMPONENT_2',
+ },
+ },
+ isOpen: true,
+ isMaximized: false,
+ size: 'large',
+ };
+
+ const result = modalManager(initialState, {
+ type: MAXIMIZE,
+ data: {},
+ });
+
+ const expected = Object.assign({}, initialState, { isMaximized: true });
+
+ expect(result).toEqual(expected);
+ });
+
+ it('should return state after minimize action', () => {
+ const initialState = {
+ componentKeys: ['COMPONENT_1', 'COMPONENT_2'],
+ components: {
+ COMPONENT_1: {
+ name: 'Component1',
+ props: {},
+ key: 'COMPONENT_1',
+ },
+ COMPONENT_2: {
+ name: 'Component2',
+ props: {},
+ key: 'COMPONENT_2',
+ },
+ },
+ isOpen: true,
+ isMaximized: true,
+ size: 'large',
+ };
+
+ const result = modalManager(initialState, {
+ type: MINIMIZE,
+ data: {},
+ });
+
+ const expected = Object.assign({}, initialState, { isMaximized: false });
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/packages/terra-clinical-modal-manager/tests/nightwatch.conf.js b/packages/terra-clinical-modal-manager/tests/nightwatch.conf.js
new file mode 100644
index 000000000..abf11f43d
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/nightwatch.conf.js
@@ -0,0 +1,9 @@
+/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
+
+const testSettings = require('terra-toolkit').testSettings;
+const resolve = require('path').resolve;
+const nightwatchConfiguration = require('terra-toolkit/lib/nightwatch.json');
+
+module.exports = (settings => (
+ testSettings(resolve('../../webpack.config'), settings)
+))(nightwatchConfiguration);
diff --git a/packages/terra-clinical-modal-manager/tests/nightwatch/DemoContainer.jsx b/packages/terra-clinical-modal-manager/tests/nightwatch/DemoContainer.jsx
new file mode 100644
index 000000000..3485c8936
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/nightwatch/DemoContainer.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import AppDelegate from 'terra-clinical-app-delegate';
+
+let nestedComponentIndex = 0;
+
+class DemoContainer extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.disclose = this.disclose.bind(this);
+ this.dismiss = this.dismiss.bind(this);
+ this.closeDisclosure = this.closeDisclosure.bind(this);
+ this.goBack = this.goBack.bind(this);
+ this.maximize = this.maximize.bind(this);
+ this.minimize = this.minimize.bind(this);
+ }
+
+ disclose() {
+ this.props.app.disclose({
+ preferredType: 'modal',
+ size: 'small',
+ content: {
+ key: `DemoContainer-${nestedComponentIndex += 1}`,
+ name: 'DemoContainer',
+ props: {
+ identifier: `DemoContainer-${nestedComponentIndex}`,
+ },
+ },
+ });
+ }
+
+ dismiss() {
+ this.props.app.dismiss();
+ }
+
+ closeDisclosure() {
+ this.props.app.closeDisclosure();
+ }
+
+ goBack() {
+ this.props.app.goBack();
+ }
+
+ maximize() {
+ this.props.app.maximize();
+ }
+
+ minimize() {
+ this.props.app.minimize();
+ }
+
+ render() {
+ const { app, identifier } = this.props;
+
+ return (
+
+
Content Component
+
+ id: {identifier}
+
+ Disclose
+ {app && app.dismiss ? Dismiss : null }
+ {app && app.closeDisclosure ? Close Disclosure : null }
+ {app && app.goBack ? Go Back : null }
+ {app && app.maximize ? Maximize : null }
+ {app && app.minimize ? Minimize : null }
+
+ );
+ }
+}
+
+DemoContainer.propTypes = {
+ app: AppDelegate.propType,
+ identifier: PropTypes.string,
+};
+
+AppDelegate.registerComponentForDisclosure('DemoContainer', DemoContainer);
+
+export default DemoContainer;
diff --git a/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerDemo.jsx b/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerDemo.jsx
new file mode 100644
index 000000000..3c49aed8a
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerDemo.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { createStore, combineReducers } from 'redux';
+import { Provider } from 'react-redux';
+
+import ModalManager, { reducers as modalManagerReducers } from '../../lib/index';
+import DemoContainer from './DemoContainer';
+
+const store = createStore(
+ combineReducers(Object.assign({},
+ modalManagerReducers,
+ )),
+);
+
+const ModalManagerDemo = () => (
+
+
+
+
+
+);
+
+export default ModalManagerDemo;
diff --git a/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerTestRoutes.jsx b/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerTestRoutes.jsx
new file mode 100644
index 000000000..8a4379b97
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerTestRoutes.jsx
@@ -0,0 +1,17 @@
+/* eslint-disable import/no-extraneous-dependencies */
+
+import React from 'react';
+import { Route } from 'react-router';
+import ModalManagerTests from './ModalManagerTests';
+
+// Test Cases
+import ModalManagerDemo from './ModalManagerDemo';
+
+const routes = (
+
+
+
+
+);
+
+export default routes;
diff --git a/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerTests.jsx b/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerTests.jsx
new file mode 100644
index 000000000..e96ba5d99
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/nightwatch/ModalManagerTests.jsx
@@ -0,0 +1,14 @@
+/* eslint-disable import/no-extraneous-dependencies */
+
+import React from 'react';
+import { Link } from 'react-router';
+
+const ModalManagerTests = () => (
+
+);
+
+export default ModalManagerTests;
diff --git a/packages/terra-clinical-modal-manager/tests/nightwatch/modal-manager-spec.js b/packages/terra-clinical-modal-manager/tests/nightwatch/modal-manager-spec.js
new file mode 100644
index 000000000..6fff43ca7
--- /dev/null
+++ b/packages/terra-clinical-modal-manager/tests/nightwatch/modal-manager-spec.js
@@ -0,0 +1,108 @@
+/* eslint import/no-extraneous-dependencies: ["error", {"devDependencies": true}] */
+/* eslint-disable no-unused-expressions */
+
+const screenshot = require('terra-toolkit').screenshot;
+
+module.exports = {
+ before: (browser, done) => {
+ browser.resizeWindow(browser.globals.width, browser.globals.height, done);
+ },
+
+ afterEach: (browser, done) => {
+ screenshot(browser, 'terra-clinical-modal-manager', done);
+ },
+
+ 'Renders the ModalManager': (browser) => {
+ browser.url(`http://localhost:${browser.globals.webpackDevServerPort}/#/tests/modal-manager-tests/demo`);
+
+ browser.expect.element('.terraClinical-ModalManager').to.be.present;
+ },
+
+ 'Opens the modal when disclose is selected': (browser) => {
+ browser.url(`http://localhost:${browser.globals.webpackDevServerPort}/#/tests/modal-manager-tests/demo`);
+
+ browser.click('#root-component .disclose');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+ },
+
+ 'Ensures goBack is not provided to a single modal component': (browser) => {
+ browser.url(`http://localhost:${browser.globals.webpackDevServerPort}/#/tests/modal-manager-tests/demo`);
+
+ browser.click('#root-component .disclose');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1 button.go-back').to.not.be.present;
+ },
+
+ 'Closes the modal when dismiss is selected within modal with one modal component': (browser) => {
+ browser.url(`http://localhost:${browser.globals.webpackDevServerPort}/#/tests/modal-manager-tests/demo`);
+
+ browser.click('#root-component .disclose');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+
+ browser.click('#DemoContainer-1 .dismiss');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.not.be.present;
+ },
+
+ 'Closes the modal when closeDisclosure is selected within modal': (browser) => {
+ browser.url(`http://localhost:${browser.globals.webpackDevServerPort}/#/tests/modal-manager-tests/demo`);
+
+ browser.click('#root-component .disclose');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+
+ browser.click('#DemoContainer-1 .close-disclosure');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.not.be.present;
+ },
+
+ 'Maximizes/minimizes the modal when selected within modal': (browser) => {
+ browser.url(`http://localhost:${browser.globals.webpackDevServerPort}/#/tests/modal-manager-tests/demo`);
+
+ browser.click('#root-component .disclose');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+ browser.expect.element('#DemoContainer-1 .maximize').to.be.present;
+
+ browser.click('#DemoContainer-1 .maximize');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+ browser.expect.element('#DemoContainer-1 .maximize').to.not.be.present;
+ browser.expect.element('#DemoContainer-1 .minimize').to.be.present;
+
+ browser.click('#DemoContainer-1 .minimize');
+
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+ browser.expect.element('#DemoContainer-1 .maximize').to.be.present;
+ browser.expect.element('#DemoContainer-1 .minimize').to.not.be.present;
+ },
+
+ 'Discloses within the modal when modal is already presented': (browser) => {
+ browser.url(`http://localhost:${browser.globals.webpackDevServerPort}/#/tests/modal-manager-tests/demo`);
+
+ browser.click('#root-component .disclose');
+
+ browser.expect.element('.terraClinical-ModalManager-modal .terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+
+ browser.click('#DemoContainer-1 .disclose');
+
+ // Waiting here to ensure new component is presented and back button is clickable
+ browser.waitForElementPresent('.terraClinical-Slide:not(.terraClinical-Slide-enter-active):nth-child(2)', 350);
+
+ browser.expect.element('.terraClinical-ModalManager-modal .terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+ browser.expect.element('.terraClinical-ModalManager-modal .terraClinical-SlideGroup #DemoContainer-2').to.be.present;
+ browser.expect.element('.terraClinical-SlideGroup #DemoContainer-2 button.go-back').to.be.present;
+
+ browser.click('#DemoContainer-2 button.go-back');
+
+ browser.waitForElementNotPresent('#DemoContainer-2', 1000);
+
+ browser.expect.element('.terraClinical-ModalManager-modal .terraClinical-SlideGroup #DemoContainer-1').to.be.present;
+ browser.expect.element('.terraClinical-ModalManager-modal .terraClinical-SlideGroup #DemoContainer-2').to.not.be.present;
+ },
+};
+
diff --git a/packages/terra-clinical-site/package.json b/packages/terra-clinical-site/package.json
index 3428336cc..83493607e 100644
--- a/packages/terra-clinical-site/package.json
+++ b/packages/terra-clinical-site/package.json
@@ -31,6 +31,8 @@
"react-dom": "^15.4.2",
"react-intl": "^2.3.0",
"react-router": "^3.0.5",
+ "react-redux": "^5.0.4",
+ "redux": "^3.6.0",
"terra-base": "^0.x",
"terra-button": "^0.x",
"terra-clinical-action-header": "^0.x",
@@ -44,6 +46,7 @@
"terra-clinical-label-value-view": "^0.x",
"terra-clinical-no-data-view": "^0.x",
"terra-clinical-slide-group": "^0.x",
+ "terra-clinical-modal-manager": "^0.x",
"terra-icon": "^0.x",
"terra-grid": "^2.x",
"terra-legacy-theme": "^0.x",
diff --git a/packages/terra-clinical-site/src/App.jsx b/packages/terra-clinical-site/src/App.jsx
index 8449c29d9..f18e4a4e5 100644
--- a/packages/terra-clinical-site/src/App.jsx
+++ b/packages/terra-clinical-site/src/App.jsx
@@ -34,6 +34,7 @@ const App = props => (
Label Value View} />
Slide Group} />
No Data View} />
+ Modal Manager} />
Tests} />
diff --git a/packages/terra-clinical-site/src/Index.jsx b/packages/terra-clinical-site/src/Index.jsx
index d843d15c9..a608f2828 100644
--- a/packages/terra-clinical-site/src/Index.jsx
+++ b/packages/terra-clinical-site/src/Index.jsx
@@ -17,6 +17,7 @@ import HeaderExamples from './examples/header/Index';
import LabelValueViewExamples from './examples/label-value-view/Index';
import NoDataViewExamples from './examples/no-data-view/Index';
import SlideGroupExamples from './examples/slide-group/Index';
+import ModalManagerExamples from './examples/modal-manager/Index';
// Test
/* eslint-disable import/first */
@@ -34,6 +35,8 @@ import HeaderTestRoutes from 'terra-clinical-header/tests/nightwatch/HeaderTestR
import LabelValueViewTestRoutes from 'terra-clinical-label-value-view/tests/nightwatch/LabelValueViewTestRoutes';
import NoDataViewTestRoutes from 'terra-clinical-no-data-view/tests/nightwatch/NoDataViewTestRoutes';
import SlideGroupTestRoutes from 'terra-clinical-slide-group/tests/nightwatch/SlideGroupTestRoutes';
+import ModalManagerTestRoutes from 'terra-clinical-modal-manager/tests/nightwatch/ModalManagerTestRoutes';
+
import TestLinks from './TestLinks';
/* eslint-enable import/first */
@@ -53,6 +56,7 @@ ReactDOM.render((
+
{ActionHeaderTestRoutes}
@@ -68,5 +72,6 @@ ReactDOM.render((
{LabelValueViewTestRoutes}
{NoDataViewTestRoutes}
{SlideGroupTestRoutes}
+ {ModalManagerTestRoutes}
), document.getElementById('root'));
diff --git a/packages/terra-clinical-site/src/TestLinks.jsx b/packages/terra-clinical-site/src/TestLinks.jsx
index a4c5fc660..f81595f6a 100644
--- a/packages/terra-clinical-site/src/TestLinks.jsx
+++ b/packages/terra-clinical-site/src/TestLinks.jsx
@@ -18,6 +18,7 @@ const TestLinks = () => (
Label Value View Tests
NoDataView Tests
SlideGroup Tests
+ ModalManager Tests
);
diff --git a/packages/terra-clinical-site/src/examples/modal-manager/ContentContainer.jsx b/packages/terra-clinical-site/src/examples/modal-manager/ContentContainer.jsx
new file mode 100644
index 000000000..96a519f55
--- /dev/null
+++ b/packages/terra-clinical-site/src/examples/modal-manager/ContentContainer.jsx
@@ -0,0 +1,87 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import AppDelegate from 'terra-clinical-app-delegate';
+
+class ContentContainer extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.disclose = this.disclose.bind(this);
+ this.dismiss = this.dismiss.bind(this);
+ this.closeDisclosure = this.closeDisclosure.bind(this);
+ this.goBack = this.goBack.bind(this);
+ this.maximize = this.maximize.bind(this);
+ this.minimize = this.minimize.bind(this);
+ }
+
+ disclose(size) {
+ return () => {
+ const identifier = Date.now();
+
+ this.props.app.disclose({
+ preferredType: 'modal',
+ size,
+ content: {
+ key: `ContentContainer-${identifier}`,
+ name: 'ContentContainer',
+ props: {
+ identifier: `ContentContainer-${identifier}`,
+ },
+ },
+ });
+ };
+ }
+
+ dismiss() {
+ this.props.app.dismiss();
+ }
+
+ closeDisclosure() {
+ this.props.app.closeDisclosure();
+ }
+
+ goBack() {
+ this.props.app.goBack();
+ }
+
+ maximize() {
+ this.props.app.maximize();
+ }
+
+ minimize() {
+ this.props.app.minimize();
+ }
+
+ render() {
+ const { app, identifier } = this.props;
+
+ return (
+
+
Content Component
+
+ id: {identifier}
+
+ Disclose
+ {identifier === 'root-component' && Disclose - Tiny }
+ {identifier === 'root-component' && Disclose - Small }
+ {identifier === 'root-component' && Disclose - Medium }
+ {identifier === 'root-component' && Disclose - Large }
+ {identifier === 'root-component' && Disclose - Huge }
+ {app && app.dismiss ? Dismiss : null }
+ {app && app.closeDisclosure ? Close Disclosure : null }
+ {app && app.goBack ? Go Back : null }
+ {app && app.maximize ? Maximize : null }
+ {app && app.minimize ? Minimize : null }
+
+ );
+ }
+}
+
+ContentContainer.propTypes = {
+ app: AppDelegate.propType,
+ identifier: PropTypes.string,
+};
+
+AppDelegate.registerComponentForDisclosure('ContentContainer', ContentContainer);
+
+export default ContentContainer;
diff --git a/packages/terra-clinical-site/src/examples/modal-manager/Index.jsx b/packages/terra-clinical-site/src/examples/modal-manager/Index.jsx
new file mode 100644
index 000000000..6a106def2
--- /dev/null
+++ b/packages/terra-clinical-site/src/examples/modal-manager/Index.jsx
@@ -0,0 +1,29 @@
+/* eslint-disable import/no-extraneous-dependencies */
+import React from 'react';
+import PropsTable from 'terra-props-table';
+import Markdown from 'terra-markdown';
+import ReadMe from 'terra-clinical-modal-manager/docs/README.md';
+import { version } from 'terra-clinical-modal-manager/package.json';
+
+// Component Source
+// eslint-disable-next-line import/no-webpack-loader-syntax, import/first, import/no-unresolved, import/extensions
+import ModalManagerSrc from '!raw-loader!terra-clinical-modal-manager/src/ModalManager';
+
+import ModalManagerDemo from './ModalManagerDemo';
+
+// Example Files
+
+const ModalManagerExamples = () => (
+
+
Version: {version}
+
+
+
+
Demo
+
+
+
+
+);
+
+export default ModalManagerExamples;
diff --git a/packages/terra-clinical-site/src/examples/modal-manager/ModalManagerDemo.jsx b/packages/terra-clinical-site/src/examples/modal-manager/ModalManagerDemo.jsx
new file mode 100644
index 000000000..6a8dc7f62
--- /dev/null
+++ b/packages/terra-clinical-site/src/examples/modal-manager/ModalManagerDemo.jsx
@@ -0,0 +1,22 @@
+import React from 'react';
+import { createStore, combineReducers } from 'redux';
+import { Provider } from 'react-redux';
+
+import ModalManager, { reducers as modalManagerReducers } from 'terra-clinical-modal-manager';
+import ContentContainer from './ContentContainer';
+
+const store = createStore(
+ combineReducers(Object.assign({},
+ modalManagerReducers,
+ )),
+);
+
+const ModalManagerDemo = () => (
+
+
+
+
+
+);
+
+export default ModalManagerDemo;
diff --git a/packages/terra-clinical-site/src/examples/slide-group/SlideGroupDemo.jsx b/packages/terra-clinical-site/src/examples/slide-group/SlideGroupDemo.jsx
index 07893e708..b794b9141 100644
--- a/packages/terra-clinical-site/src/examples/slide-group/SlideGroupDemo.jsx
+++ b/packages/terra-clinical-site/src/examples/slide-group/SlideGroupDemo.jsx
@@ -1,4 +1,5 @@
import React from 'react';
+import PropTypes from 'prop-types';
import SlideGroup from 'terra-clinical-slide-group';
@@ -64,7 +65,7 @@ class SlideGroupDemo extends React.Component {
}
SlideGroupDemo.propTypes = {
- isAnimated: React.PropTypes.bool,
+ isAnimated: PropTypes.bool,
};
export default SlideGroupDemo;
diff --git a/packages/terra-clinical-slide-group/docs/README.md b/packages/terra-clinical-slide-group/docs/README.md
index 7908817db..6a4d4986e 100644
--- a/packages/terra-clinical-slide-group/docs/README.md
+++ b/packages/terra-clinical-slide-group/docs/README.md
@@ -10,6 +10,7 @@ marked with the `aria-hidden` attribute, they are not actually unmounted.
- Install with [npmjs](https://www.npmjs.com):
- `npm install terra-clinical-slide-group`
+ - `yarn add terra-clinical-slide-group`
## Usage
diff --git a/packages/terra-clinical-slide-group/package.json b/packages/terra-clinical-slide-group/package.json
index fcbcb67b2..d6250cd3b 100644
--- a/packages/terra-clinical-slide-group/package.json
+++ b/packages/terra-clinical-slide-group/package.json
@@ -11,6 +11,7 @@
"keywords": [
"Cerner",
"Terra",
+ "Clinical",
"terra-clinical-slide-group",
"SlideGroup",
"UI"