diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8803543d2..74f1eb9d1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@
* Adding SessionContext
* ApplicationBase updates
* Adding SkipToLink
+ * Adding MainContainer
## Unreleased
diff --git a/package.json b/package.json
index a9b53ea04..7083e1e20 100644
--- a/package.json
+++ b/package.json
@@ -7,7 +7,7 @@
"version": "1.35.0",
"description": "A framework to support application development with Terra components",
"engines": {
- "node": ">=10"
+ "node": ">=10 <13"
},
"repository": {
"type": "git",
@@ -104,7 +104,6 @@
"terra-slide-panel-manager": "^5.19.0",
"terra-status-view": "^4.10.0",
"terra-theme-context": "^1.0.0",
- "terra-theme-provider": "^4.0.0",
"terra-toolbar": "^1.20.0",
"terra-visually-hidden-text": "^2.30.0",
"uuid": "^3.0.0",
diff --git a/src/application-container/ApplicationContainer.jsx b/src/application-container/ApplicationContainer.jsx
index e822beccd..705d896a4 100644
--- a/src/application-container/ApplicationContainer.jsx
+++ b/src/application-container/ApplicationContainer.jsx
@@ -4,6 +4,7 @@ import classNames from 'classnames/bind';
import { NavigationPromptCheckpoint } from '../navigation-prompt';
import WindowManager from '../utils/window-manager';
+import ActiveMainProvider from '../main-container/private/ActiveMainProvider';
import ApplicationContainerErrorBoundary from './private/ApplicationContainerErrorBoundary';
import useSkipToLinks from './private/skip-to-links/useSkipToLinks';
@@ -63,9 +64,11 @@ const ApplicationContainer = ({
-
- {children}
-
+
+
+ {children}
+
+
diff --git a/src/application-container/index.js b/src/application-container/index.js
index 3e305f44e..9a1443435 100644
--- a/src/application-container/index.js
+++ b/src/application-container/index.js
@@ -1,5 +1,4 @@
import ApplicationContainer from './ApplicationContainer';
-import useActiveMainPage from './useActiveMainPage';
import ApplicationContainerContext, {
useApplicationContainer,
contextShape as applicationContainerContextShape,
@@ -11,7 +10,6 @@ import ApplicationConceptContext, {
export default ApplicationContainer;
export {
- useActiveMainPage,
ApplicationContainerContext,
useApplicationContainer,
applicationContainerContextShape,
diff --git a/src/application-container/private/active-main-page/ActiveMainPageContext.jsx b/src/application-container/private/active-main-page/ActiveMainPageContext.jsx
deleted file mode 100644
index 22deb18a3..000000000
--- a/src/application-container/private/active-main-page/ActiveMainPageContext.jsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { createContext } from 'react';
-import PropTypes from 'prop-types';
-
-const ActiveMainPageContext = createContext();
-
-const contextShape = {
- parentNavigationKeys: PropTypes.array,
- pageKey: PropTypes.string,
- pageLabel: PropTypes.string,
- pageMetaData: PropTypes.object,
-};
-
-export default ActiveMainPageContext;
-export { contextShape };
diff --git a/src/application-container/useActiveMainPage.jsx b/src/application-container/useActiveMainPage.jsx
deleted file mode 100644
index 24c54f106..000000000
--- a/src/application-container/useActiveMainPage.jsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-
-import ActiveMainPageContext from './private/active-main-page/ActiveMainPageContext';
-
-const useActiveMainPage = () => {
- const activeMainPage = React.useContext(ActiveMainPageContext);
-
- return activeMainPage;
-};
-
-export default useActiveMainPage;
diff --git a/src/main-container/ActiveMainContext.jsx b/src/main-container/ActiveMainContext.jsx
new file mode 100644
index 000000000..878dc7171
--- /dev/null
+++ b/src/main-container/ActiveMainContext.jsx
@@ -0,0 +1,28 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * The ActiveMainContext is used to communicate data related to the current
+ * active main content to the application.
+ */
+const ActiveMainContext = React.createContext({});
+
+/**
+ * Hook to simplify consumption of the ActiveMainContext.
+ * @returns The ActiveMainContext value found at the consuming render location.
+ */
+const useActiveMain = () => React.useContext(ActiveMainContext);
+
+const contextShape = {
+ /**
+ * The string label describing the active main content to be used for display purposes.
+ */
+ label: PropTypes.string,
+ /**
+ * A collection of data related to the active main content.
+ */
+ metaData: PropTypes.object,
+};
+
+export default ActiveMainContext;
+export { useActiveMain, contextShape };
diff --git a/src/main-container/MainContainer.jsx b/src/main-container/MainContainer.jsx
new file mode 100644
index 000000000..311ff8509
--- /dev/null
+++ b/src/main-container/MainContainer.jsx
@@ -0,0 +1,105 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import classNamesBind from 'classnames/bind';
+
+import { ApplicationIntlContext } from '../application-intl';
+import SkipToLink from '../application-container/private/skip-to-links/SkipToLink';
+import NavigationItemContext from '../navigation-item/NavigationItemContext';
+
+import ActiveMainRegistrationContext from './private/ActiveMainRegistrationContext';
+
+import styles from './MainContainer.module.scss';
+
+const cx = classNamesBind.bind(styles);
+
+const propTypes = {
+ /**
+ * The elements to render within the main element.
+ */
+ children: PropTypes.node,
+ /**
+ * A string label describing the content within the main element. This value
+ * will be applied to the element as a user-facing aria-label and should be
+ * translated, if necessary. It will also be provided to consumers of the
+ * ActiveMainContext when this element is active.
+ */
+ label: PropTypes.string.isRequired,
+ /**
+ * An object containing meta data related to the main element. This data is
+ * provided to consumers of the ActiveMainContext to provide additional
+ * information regarding the active main content.
+ */
+ metaData: PropTypes.object,
+ /**
+ * A function to be called when a ref has been assigned for the created
+ * `` element.
+ */
+ refCallback: PropTypes.func,
+};
+
+/**
+ * The MainContainer can be used to create a semantic `` element for the
+ * application, within which the application's most important and dynamic
+ * content will reside. A SkipToLink will be registered automatically to ensure
+ * this content can be accessed quickly.
+ */
+const MainContainer = ({
+ children, refCallback, label, metaData, ...otherProps
+}) => {
+ const applicationIntl = React.useContext(ApplicationIntlContext);
+ const activeMainRegistration = React.useContext(ActiveMainRegistrationContext);
+ const navigationItem = React.useContext(NavigationItemContext);
+
+ const mainElementRef = React.useRef();
+ const unregisterActiveMainRef = React.useRef();
+
+ React.useEffect(() => {
+ unregisterActiveMainRef.current = activeMainRegistration.register({
+ label,
+ metaData,
+ });
+ }, [
+ activeMainRegistration,
+ label,
+ metaData,
+ navigationItem.isActive,
+ navigationItem.navigationKeys,
+ ]);
+
+ React.useEffect(() => () => {
+ // A separate effect is used to unregister the active main when it is
+ // unmounted to limit registration thrash on updates to props.
+ unregisterActiveMainRef.current();
+ }, []);
+
+ return (
+ {
+ mainElementRef.current = mainRef;
+
+ if (refCallback) {
+ refCallback(mainRef);
+ }
+ }}
+ {...otherProps}
+ >
+ {
+ mainElementRef.current.focus();
+ }}
+ />
+ {children}
+
+ );
+};
+
+MainContainer.propTypes = propTypes;
+
+export default MainContainer;
diff --git a/src/main-container/MainContainer.module.scss b/src/main-container/MainContainer.module.scss
new file mode 100644
index 000000000..326d756a5
--- /dev/null
+++ b/src/main-container/MainContainer.module.scss
@@ -0,0 +1,5 @@
+:local {
+ .main-container {
+ outline: none;
+ }
+}
diff --git a/src/main-container/index.js b/src/main-container/index.js
new file mode 100644
index 000000000..2df8883bb
--- /dev/null
+++ b/src/main-container/index.js
@@ -0,0 +1,8 @@
+import MainContainer from './MainContainer';
+import ActiveMainContext, {
+ useActiveMain,
+ contextShape as activeMainContextShape,
+} from './ActiveMainContext';
+
+export default MainContainer;
+export { ActiveMainContext, useActiveMain, activeMainContextShape };
diff --git a/src/main-container/private/ActiveMainProvider.jsx b/src/main-container/private/ActiveMainProvider.jsx
new file mode 100644
index 000000000..c9e81e778
--- /dev/null
+++ b/src/main-container/private/ActiveMainProvider.jsx
@@ -0,0 +1,106 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import uuidv4 from 'uuid/v4';
+
+import NavigationItemContext from '../../navigation-item/NavigationItemContext';
+
+import ActiveMainContext from '../ActiveMainContext';
+import ActiveMainRegistrationContext from './ActiveMainRegistrationContext';
+
+const propTypes = {
+ children: PropTypes.node,
+};
+
+const ActiveMainProvider = ({ children }) => {
+ const navigationItem = React.useContext(NavigationItemContext);
+ const activeMainRegistration = React.useContext(ActiveMainRegistrationContext);
+ const unregisterActiveMainRef = React.useRef();
+
+ const [state, dispatch] = React.useReducer((currentState, action) => {
+ if (action.type === 'register') {
+ return {
+ registrationId: action.registrationId,
+ activeMain: {
+ label: action.label,
+ metaData: action.metaData,
+ },
+ };
+ }
+
+ if (action.type === 'unregister') {
+ if (currentState.registrationId === action.registrationId) {
+ return {
+ registrationId: undefined,
+ activeMainPage: undefined,
+ };
+ }
+ }
+
+ return currentState;
+ }, { registrationId: undefined, activeMain: undefined });
+
+ React.useEffect(() => {
+ if (!activeMainRegistration) {
+ return;
+ }
+
+ // If an ancestor provider exists, we need to forward the active main info
+ // if the provider exists in the active navigation branch. It is
+ // otherwise unregistered, if necessary, as we do not want potentially stale
+ // information living above this provider level.
+ if (navigationItem.isActive) {
+ unregisterActiveMainRef.current = activeMainRegistration.register(state.activeMain);
+ } else if (unregisterActiveMainRef.current) {
+ unregisterActiveMainRef.current();
+ unregisterActiveMainRef.current = undefined;
+ }
+ }, [state.activeMain, navigationItem.isActive, activeMainRegistration]);
+
+ React.useEffect(() => () => {
+ if (unregisterActiveMainRef.current) {
+ unregisterActiveMainRef.current();
+ unregisterActiveMainRef.current = undefined;
+ }
+ }, []);
+
+ const activeMainRegistrationContextValue = React.useMemo(() => ({
+ register: (registrationData) => {
+ if (!registrationData) {
+ return undefined;
+ }
+
+ const { label, metaData } = registrationData;
+
+ // Multiple main sources can be writing to this single provider, and the
+ // order of their registrations is not guaranteed to be deterministic.
+ // So we assign an identifier to each registration and check it above
+ // prior to performing any unregistrations, ensuring that main elements
+ // can execute their unregistration logic as part of their lifecycle
+ // without worrying about damaging registration data from other sources.
+ const registrationId = uuidv4();
+
+ dispatch({
+ type: 'register',
+ registrationId,
+ label,
+ metaData,
+ });
+
+ return () => {
+ dispatch({ type: 'unregister', registrationId });
+ };
+ },
+ }), []);
+
+ return (
+
+
+ {children}
+
+
+ );
+};
+
+ActiveMainProvider.propTypes = propTypes;
+
+export default ActiveMainProvider;
diff --git a/src/main-container/private/ActiveMainRegistrationContext.jsx b/src/main-container/private/ActiveMainRegistrationContext.jsx
new file mode 100644
index 000000000..24b74df79
--- /dev/null
+++ b/src/main-container/private/ActiveMainRegistrationContext.jsx
@@ -0,0 +1,19 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * A private Context used to enable communication between the
+ * ActiveMainPageProvider and the MainContainer.
+ */
+const ActiveMainPageRegistrationContext = React.createContext();
+
+const contextShape = {
+ /**
+ * A function used to register page data.
+ * Returns a function that will undo the registration.
+ */
+ register: PropTypes.func,
+};
+
+export default ActiveMainPageRegistrationContext;
+export { contextShape };
diff --git a/src/navigation-item/NavigationItem.jsx b/src/navigation-item/NavigationItem.jsx
index 0cdb02e9d..f1c7cc08f 100644
--- a/src/navigation-item/NavigationItem.jsx
+++ b/src/navigation-item/NavigationItem.jsx
@@ -2,6 +2,8 @@ import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
+import ActiveMainProvider from '../main-container/private/ActiveMainProvider';
+
import NavigationItemContext from './NavigationItemContext';
const propTypes = {
@@ -77,7 +79,9 @@ const NavigationItem = ({
return ReactDOM.createPortal((
- {pageContent}
+
+ {pageContent}
+
), portalElement);
};
diff --git a/src/terra-dev-site/test/main-container/MainContainerNavigation.test.jsx b/src/terra-dev-site/test/main-container/MainContainerNavigation.test.jsx
new file mode 100644
index 000000000..233c4c3b3
--- /dev/null
+++ b/src/terra-dev-site/test/main-container/MainContainerNavigation.test.jsx
@@ -0,0 +1,118 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import ApplicationBase from '../../../application-base';
+import ApplicationContainer from '../../../application-container';
+import MainContainer, { useActiveMain } from '../../../main-container';
+import PrimaryNavigationLayout, { NavigationItem } from '../../../primary-navigation-layout';
+
+const ActiveMainConsumer = ({ label }) => {
+ const activeMain = useActiveMain();
+
+ if (!activeMain) {
+ return
No Active Main
;
+ }
+
+ return (
+
+ {label ?
{label}
: undefined}
+
+ Active Main Label:
+ {' '}
+ {activeMain.label}
+
+
+ Active Main Metadata:
+ {' '}
+ {JSON.stringify(activeMain.metaData)}
+
- Active Main Page Key:
+ Active Main Label:
{' '}
- {activeMainPage?.pageKey}
+ {activeMain?.label}
- Active Main Page Label:
+ Active Main MetaData:
{' '}
- {activeMainPage?.pageLabel}
-
-
- Active Main Page MetaData:
- {' '}
- {`${JSON.stringify(activeMainPage?.pageMetaData)}`}
+ {`${JSON.stringify(activeMain?.metaData)}`}
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nam sit amet lacus cursus massa ullamcorper scelerisque. Aenean vitae posuere neque, consequat dapibus diam. Proin convallis venenatis magna, sit amet volutpat erat. Nulla sodales eu est sit amet sagittis. Suspendisse lacinia diam ut justo venenatis, a sollicitudin ante venenatis. Vivamus a leo ullamcorper, tristique diam id, vestibulum felis. Nullam mattis eget eros vestibulum porttitor. Duis eget massa nec urna ultrices laoreet. Aenean rhoncus mauris in luctus blandit. Morbi tempor enim a libero placerat, at bibendum elit luctus. In in tempor neque, laoreet facilisis quam. Fusce faucibus dui eget erat gravida egestas. Nullam laoreet purus eget urna placerat, sit amet ultrices mi tristique. Sed vulputate gravida risus, vehicula rhoncus ipsum tempus id. Phasellus aliquet nec mi non pretium. Maecenas turpis nulla, mollis et rhoncus vel, porttitor id nisl.
diff --git a/src/terra-dev-site/test/workspace/OverlayWorkspace.test.jsx b/src/terra-dev-site/test/workspace/OverlayWorkspace.test.jsx
index dfcf44030..16ad1f7c5 100644
--- a/src/terra-dev-site/test/workspace/OverlayWorkspace.test.jsx
+++ b/src/terra-dev-site/test/workspace/OverlayWorkspace.test.jsx
@@ -1,7 +1,7 @@
import React from 'react';
import ApplicationBase from '../../../application-base';
import Workspace, { WorkspaceItem } from '../../../workspace';
-import ActiveMainPageContext from '../../../application-container/private/active-main-page/ActiveMainPageContext';
+import ActiveMainContext from '../../../main-container/ActiveMainContext';
import Tab1 from './Tab1';
import Tab2 from './Tab2';
import Tab3 from './Tab3';
@@ -19,9 +19,9 @@ const WorkspaceTest = () => {
const [activeItemKey, setActiveItemKey] = React.useState('tab-1');
const [workspaceSize, setWorkspaceSize] = React.useState('large');
const activeMainPageRef = React.useRef({
- pageKey: 'page-1',
- pageLabel: 'Test Page',
- pageMetaData: {
+ id: 'page-1',
+ label: 'Test Page',
+ metaData: {
data: 'data here',
},
});
@@ -36,7 +36,7 @@ const WorkspaceTest = () => {
return (
-
+