- Updated state handling in DashboardTabPanes to ensure null safety for tab state properties. - Introduced a new utility function to determine if a location belongs to a leaving tab, enhancing tab navigation logic. - Refactored location comparison logic to improve accuracy when checking for equality between tab states. - Enhanced NavigationTabsContext to manage same-path query cleanup, improving tab restoration behavior.
245 lines
6.8 KiB
JavaScript
245 lines
6.8 KiB
JavaScript
import { memo, useLayoutEffect, useMemo, useRef } from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import classNames from 'classnames'
|
|
import { Outlet, Routes, UNSAFE_LocationContext, useLocation } from 'react-router-dom'
|
|
import { TableStateProvider } from '../context/TableStateContext'
|
|
import {
|
|
NavigationTabActiveContext,
|
|
NavigationTabIdContext,
|
|
createNavigationTabActiveStore,
|
|
useNavigationTabs
|
|
} from '../context/NavigationTabsContext'
|
|
import { wrapDashboardChildRoutes } from './DashboardChildRoutes'
|
|
|
|
const locationsEqual = (left, right) =>
|
|
Boolean(
|
|
left &&
|
|
right &&
|
|
left.pathname === right.pathname &&
|
|
(left.search || '') === (right.search || '') &&
|
|
(left.hash || '') === (right.hash || '')
|
|
)
|
|
|
|
const toTabLocation = (tabId, source) => ({
|
|
pathname: source?.pathname || '/',
|
|
search: source?.search ?? '',
|
|
hash: source?.hash ?? '',
|
|
state: source?.state ?? null,
|
|
key: `tab-${tabId}`
|
|
})
|
|
|
|
const reuseTabLocation = (tabId, previous, next) => {
|
|
if (
|
|
previous &&
|
|
locationsEqual(previous, next) &&
|
|
previous.key === `tab-${tabId}` &&
|
|
(previous.state ?? null) === (next.state ?? null)
|
|
) {
|
|
return previous
|
|
}
|
|
return toTabLocation(tabId, next)
|
|
}
|
|
|
|
const entryToLocation = (entry, fallbackLocation, tabId) =>
|
|
toTabLocation(tabId, {
|
|
pathname: entry?.pathname || fallbackLocation.pathname,
|
|
search: entry?.search ?? fallbackLocation.search ?? '',
|
|
hash: entry?.hash ?? fallbackLocation.hash ?? '',
|
|
state: entry?.state ?? null
|
|
})
|
|
|
|
const getTabCurrentEntry = (tab) =>
|
|
tab?.history?.[tab.historyIndex] || tab?.history?.[tab.history?.length - 1]
|
|
|
|
const tabMatchesLocation = (tab, loc) => {
|
|
const entry = getTabCurrentEntry(tab)
|
|
if (!entry || !loc) return false
|
|
return locationsEqual(entry, loc)
|
|
}
|
|
|
|
const locationBelongsToLeavingTab = (tab, frozen, loc) => {
|
|
if (!loc) return false
|
|
if (frozen && locationsEqual(frozen, loc)) return true
|
|
return tabMatchesLocation(tab, loc)
|
|
}
|
|
|
|
const CachedTabRouteLayout = () => (
|
|
<TableStateProvider>
|
|
<Outlet />
|
|
</TableStateProvider>
|
|
)
|
|
|
|
const areLocationsEqual = (left, right) =>
|
|
left === right ||
|
|
(locationsEqual(left, right) &&
|
|
left?.key === right?.key &&
|
|
(left?.state ?? null) === (right?.state ?? null))
|
|
|
|
const arePanePropsEqual = (prev, next) =>
|
|
prev.tabId === next.tabId &&
|
|
prev.isActive === next.isActive &&
|
|
prev.registerTabPane === next.registerTabPane &&
|
|
areLocationsEqual(prev.loc, next.loc)
|
|
|
|
const DashboardTabPaneRoutes = memo(function DashboardTabPaneRoutes({ loc }) {
|
|
const tabRoutes = useMemo(
|
|
() => wrapDashboardChildRoutes(<CachedTabRouteLayout />),
|
|
[]
|
|
)
|
|
const locationContextValue = useMemo(
|
|
() => ({ location: loc, navigationType: 'POP' }),
|
|
[loc]
|
|
)
|
|
|
|
return (
|
|
<UNSAFE_LocationContext.Provider value={locationContextValue}>
|
|
<Routes location={loc}>{tabRoutes}</Routes>
|
|
</UNSAFE_LocationContext.Provider>
|
|
)
|
|
}, (prev, next) => areLocationsEqual(prev.loc, next.loc))
|
|
|
|
DashboardTabPaneRoutes.displayName = 'DashboardTabPaneRoutes'
|
|
|
|
DashboardTabPaneRoutes.propTypes = {
|
|
loc: PropTypes.shape({
|
|
pathname: PropTypes.string,
|
|
search: PropTypes.string,
|
|
hash: PropTypes.string,
|
|
key: PropTypes.string,
|
|
state: PropTypes.any
|
|
}).isRequired
|
|
}
|
|
|
|
const DashboardTabPane = memo(function DashboardTabPane({
|
|
tabId,
|
|
isActive,
|
|
loc,
|
|
registerTabPane
|
|
}) {
|
|
const storeRef = useRef(null)
|
|
if (storeRef.current == null) {
|
|
storeRef.current = createNavigationTabActiveStore(isActive)
|
|
}
|
|
|
|
useLayoutEffect(() => {
|
|
storeRef.current.setActive(isActive)
|
|
}, [isActive])
|
|
|
|
return (
|
|
<div
|
|
ref={(element) => registerTabPane(tabId, element)}
|
|
data-tab-id={tabId}
|
|
className={classNames('dashboard-tab-pane', {
|
|
'dashboard-tab-pane-active': isActive,
|
|
'dashboard-tab-pane-inactive': !isActive
|
|
})}
|
|
aria-hidden={!isActive}
|
|
inert={!isActive}
|
|
>
|
|
<NavigationTabIdContext.Provider value={tabId}>
|
|
<NavigationTabActiveContext.Provider value={storeRef.current}>
|
|
<DashboardTabPaneRoutes loc={loc} />
|
|
</NavigationTabActiveContext.Provider>
|
|
</NavigationTabIdContext.Provider>
|
|
</div>
|
|
)
|
|
}, arePanePropsEqual)
|
|
|
|
DashboardTabPane.displayName = 'DashboardTabPane'
|
|
|
|
DashboardTabPane.propTypes = {
|
|
tabId: PropTypes.string.isRequired,
|
|
isActive: PropTypes.bool,
|
|
loc: PropTypes.shape({
|
|
pathname: PropTypes.string,
|
|
search: PropTypes.string,
|
|
hash: PropTypes.string,
|
|
key: PropTypes.string,
|
|
state: PropTypes.any
|
|
}).isRequired,
|
|
registerTabPane: PropTypes.func.isRequired
|
|
}
|
|
|
|
const DashboardTabPanes = () => {
|
|
const location = useLocation()
|
|
const { tabs, activeTabId, registerTabPane } = useNavigationTabs()
|
|
const frozenRef = useRef(new Map())
|
|
const prevActiveIdRef = useRef(activeTabId)
|
|
const lastLocationRef = useRef(location)
|
|
const pendingRestoreRef = useRef(false)
|
|
|
|
const prevActiveId = prevActiveIdRef.current
|
|
const switchedTabs = Boolean(prevActiveId && activeTabId && prevActiveId !== activeTabId)
|
|
|
|
if (switchedTabs) {
|
|
const previousTab = tabs.find((tab) => tab.id === prevActiveId)
|
|
const previousFrozen = frozenRef.current.get(prevActiveId)
|
|
const lastLoc = lastLocationRef.current
|
|
if (locationBelongsToLeavingTab(previousTab, previousFrozen, lastLoc)) {
|
|
frozenRef.current.set(
|
|
prevActiveId,
|
|
reuseTabLocation(prevActiveId, previousFrozen, lastLoc)
|
|
)
|
|
} else if (!previousFrozen && previousTab) {
|
|
frozenRef.current.set(
|
|
prevActiveId,
|
|
entryToLocation(getTabCurrentEntry(previousTab), lastLoc, prevActiveId)
|
|
)
|
|
}
|
|
pendingRestoreRef.current = true
|
|
}
|
|
|
|
const activeTab = tabs.find((tab) => tab.id === activeTabId)
|
|
if (activeTab && tabMatchesLocation(activeTab, location)) {
|
|
pendingRestoreRef.current = false
|
|
frozenRef.current.set(
|
|
activeTabId,
|
|
reuseTabLocation(
|
|
activeTabId,
|
|
frozenRef.current.get(activeTabId),
|
|
location
|
|
)
|
|
)
|
|
}
|
|
|
|
for (const tabId of [...frozenRef.current.keys()]) {
|
|
if (!tabs.some((tab) => tab.id === tabId)) {
|
|
frozenRef.current.delete(tabId)
|
|
}
|
|
}
|
|
|
|
prevActiveIdRef.current = activeTabId
|
|
lastLocationRef.current = location
|
|
|
|
if (tabs.length === 0) {
|
|
return <Outlet />
|
|
}
|
|
|
|
return (
|
|
<div className='dashboard-tab-panes'>
|
|
{tabs.map((tab) => {
|
|
const isActive = tab.id === activeTabId
|
|
const loc =
|
|
isActive && !pendingRestoreRef.current
|
|
? reuseTabLocation(tab.id, frozenRef.current.get(tab.id), location)
|
|
: frozenRef.current.get(tab.id) ||
|
|
entryToLocation(getTabCurrentEntry(tab), location, tab.id)
|
|
|
|
frozenRef.current.set(tab.id, loc)
|
|
|
|
return (
|
|
<DashboardTabPane
|
|
key={tab.id}
|
|
tabId={tab.id}
|
|
isActive={isActive}
|
|
loc={loc}
|
|
registerTabPane={registerTabPane}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default DashboardTabPanes
|