hoist-react
hoist-react copied to clipboard
Add new Dynamic Tab Switcher component
Overview:
At this point, we have implemented user-customizable tab switchers in a number of client apps. Ideally, we would like to promote this capability to the framework via a new DynamicTabSwitcher
component. Application developers could opt to use this component in place of our standard TabSwitcher.
Key Requirements
- [x] Allow users to “favorite” certain tabs.
- [x] Favorited tabs will appear in the switcher and can be:
- [x] Drag-and-drop re-ordered by the user
- [x] Un-favorited and consequently closed / unmounted via context-menu or dragging them out of the switcher entirely
- [x] Un-favorited but kept "open" (mounted) by dragging them into a space dedicated to “transient” tabs
- [x] “Transient” tabs (i.e. visited tabs which are not favorited) should appear in the switcher, separated from the favorited tabs by a vertical divider. As “transient” tabs, they cannot be re-ordered, and will not be persisted.
- [x] The following actions should be available for “transient” (non-favorited) tabs:
- [x] Ability to remove and consequently unmount the tab
- [x] Ability to favorite the tab via context-menu action or dragging the tab to the left of the divider separating transient tabs from favorite tabs
- [x] Additional tabs which are not favorited and not transient should appear in an overflow menu. Opening one of these tabs should add it to the list of transient tabs but NOT automatically favorite it.
- [x] Since the overflow menu is reserved for unvisited, unfavorited tabs, the switcher should handle space-related overflow by allowing the container itself to scroll. However, the native scroll bar should be hidden and replaced in the UI by buttons to the left and right of the tabs for a more cohesive aesthetic.
Changes to Existing Code
Currently, TabContainerModel
’s constructor takes the following TabContainerConfig
:
export interface TabContainerConfig {
...
/**
* Indicates whether to include a default switcher docked within this component. Specify as a
* boolean or an object containing props for a TabSwitcher component. Set to false to not
* include a switcher. Defaults to true.
*/
switcher?: boolean | TabSwitcherProps;
...
}
This is a somewhat awkward case of a model config accepting component props. Consider the following changes / enhancements:
- [x] Create a
TabSwitcherModel
for symmetry with the newly proposedDynamicTabSwitcherModel
.TabSwitcherModel
's config would take the following immutable properties (removed fromTabSwitcherProps
):
/** Relative position within the parent TabContainer. Defaults to 'top'. */
orientation?: Side;
/** True to animate the indicator when switching tabs. False (default) to change instantly. */
animate?: boolean;
/** Enable scrolling and place tabs that overflow into a menu. Default to false. */
enableOverflow?: boolean;
- [x]
TabSwitcherProps
would now be shared between the existing switcher component and the new “dynamic” component and contain the 3 remaining properties:
/** Width (in px) to render tabs. Only applies to horizontal orientations */
tabWidth?: number;
/** Minimum width (in px) to render tabs. Only applies to horizontal orientations */
tabMinWidth?: number;
/** Maximum width (in px) to render tabs. Only applies to horizontal orientations */
tabMaxWidth?: number;
- [ ] Update
TabContainerConfig.switcher
to accept one of the following:
true | // Conveniently get a standard switcher (current behavior)
'standard' | 'dynamic' | // Conveniently create the respective switcher model with its default configuration
{type: 'standard', config: TabSwitcherConfig} | {type: 'dynamic', config: DynamicTabSwitcherConfig} | // Specify a model config
TabSwitcherModel | DynamicTabSwitcherModel | // Provide a model instance
- [ ] Unless otherwise specified, a
DynamicTabSwitcherModel
created by its associatedTabContainerModel
using one of the above methods would share the samePersistenceProvider
- [ ]
TabContainerProps
would be updated to includetabSwitcherProps: TabSwitcherProps
which would work for either style of switcher and be trampolined from theTabContainer
to its childTabSwitcher | DynamicTabSwitcher
- [x] Add new
TabModel.unmount()
method - should be called when removing a tab from the switcher (akin to closing the tab) Current code:
export const tab = hoistCmp.factory({
...
render({model, className, testId}) {
let {content, isActive, renderMode, refreshContextModel} = model,
wasActivated = useRef(false);
if (!wasActivated.current && isActive) wasActivated.current = true;
if (
!isActive &&
(renderMode === 'unmountOnHide' || (renderMode === 'lazy' && !wasActivated.current))
) {
return null;
}
...
});
Promote wasActivated
to model, and change name (isMounted?
). Should be observable and set to false when unmount()
method is called.