Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding MainContainer, ActiveMainContext, and related files (#195)
* Introducing SkipToLinks to the ApplicationContainer. * Adding wdio tests * Adding CHANGELOG entry * Adding newlines * Copying files from integration branch * Updating implementation to be more straightforward. * Final API updates. Adding tests. * Adding node version range * Working around flaky tests * Removing unnecessary default * Adding comment
- Loading branch information
Showing
87 changed files
with
792 additions
and
77 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
14 changes: 0 additions & 14 deletions
14
src/application-container/private/active-main-page/ActiveMainPageContext.jsx
This file was deleted.
Oops, something went wrong.
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
* `<main>` element. | ||
*/ | ||
refCallback: PropTypes.func, | ||
}; | ||
|
||
/** | ||
* The MainContainer can be used to create a semantic `<main>` 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 ( | ||
<main | ||
aria-label={label} | ||
className={classNames(cx('main-container'), otherProps.className)} | ||
tabIndex="-1" | ||
ref={(mainRef) => { | ||
mainElementRef.current = mainRef; | ||
|
||
if (refCallback) { | ||
refCallback(mainRef); | ||
} | ||
}} | ||
{...otherProps} | ||
> | ||
<SkipToLink | ||
description={applicationIntl.formatMessage({ | ||
id: 'terraApplication.mainContainer.skipToLabel', | ||
})} | ||
onSelect={() => { | ||
mainElementRef.current.focus(); | ||
}} | ||
/> | ||
{children} | ||
</main> | ||
); | ||
}; | ||
|
||
MainContainer.propTypes = propTypes; | ||
|
||
export default MainContainer; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
:local { | ||
.main-container { | ||
outline: none; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import MainContainer from './MainContainer'; | ||
import ActiveMainContext, { | ||
useActiveMain, | ||
contextShape as activeMainContextShape, | ||
} from './ActiveMainContext'; | ||
|
||
export default MainContainer; | ||
export { ActiveMainContext, useActiveMain, activeMainContextShape }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 ( | ||
<ActiveMainRegistrationContext.Provider value={activeMainRegistrationContextValue}> | ||
<ActiveMainContext.Provider value={state.activeMain}> | ||
{children} | ||
</ActiveMainContext.Provider> | ||
</ActiveMainRegistrationContext.Provider> | ||
); | ||
}; | ||
|
||
ActiveMainProvider.propTypes = propTypes; | ||
|
||
export default ActiveMainProvider; |
19 changes: 19 additions & 0 deletions
19
src/main-container/private/ActiveMainRegistrationContext.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.