New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding MainContainer, ActiveMainContext, and related files #195
Changes from 11 commits
0f6bf9e
f780f60
65d0c4c
8a3dc20
eb8156b
bc18f16
a50dc71
549030e
45d2074
5af1eaf
521d2a7
66a30c5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
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 }; |
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; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
:local { | ||
.main-container { | ||
outline: none; | ||
} | ||
} |
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 }; |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,99 @@ | ||||||||||||
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. | ||||||||||||
Comment on lines
+47
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Having nested active main providers should never be a thing right? If that happens maybe we should just blow up? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is a thing, as the ActiveMainProviders are rendered both by the ApplicationContainer as well as by each NavigationItem (which will be inside the ApplicationContainer, and potentially nested even further if we have secondary/tertiary navigation concepts). The nested providers are what allow us to limit cross-talk between different navigation branches of the application. |
||||||||||||
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; | ||||||||||||
const registrationId = uuidv4(); | ||||||||||||
tbiethman marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||
|
||||||||||||
dispatch({ | ||||||||||||
type: 'register', | ||||||||||||
registrationId, | ||||||||||||
label, | ||||||||||||
metaData, | ||||||||||||
}); | ||||||||||||
|
||||||||||||
return () => { | ||||||||||||
dispatch({ type: 'unregister', registrationId }); | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nitpick for readability/consistency
Suggested change
|
||||||||||||
}; | ||||||||||||
}, | ||||||||||||
}), []); | ||||||||||||
|
||||||||||||
return ( | ||||||||||||
<ActiveMainRegistrationContext.Provider value={activeMainRegistrationContextValue}> | ||||||||||||
<ActiveMainContext.Provider value={state.activeMain}> | ||||||||||||
{children} | ||||||||||||
</ActiveMainContext.Provider> | ||||||||||||
</ActiveMainRegistrationContext.Provider> | ||||||||||||
); | ||||||||||||
}; | ||||||||||||
|
||||||||||||
ActiveMainProvider.propTypes = propTypes; | ||||||||||||
|
||||||||||||
export default ActiveMainProvider; |
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 }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should main have 100% height?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought about this but erred on the side of caution. If we add a height, I feel like we'd need to make the same call for overflow handling. But that is not always desired/required (for the Page use case, or example), which opens the door to all sorts of props to disable/enable things...
Long story long I'd like to avoid adding more styles here, for the time being at least. Makes me wonder if we should add a MainContainer to the eventual LegacyLayout and provide the default styling we used to provide there while keeping this component more generic.