feat: version switcher controlling menu only

This commit is contained in:
Francesca Giannino
2026-02-17 09:37:54 +01:00
parent fa7296fe33
commit ddbf921a2f
7 changed files with 273 additions and 124 deletions

View File

@@ -214,6 +214,46 @@
background-color: var(--color-bg-inverse);
}
.sidebar-nav-item--nested.sidebar-nav-item--inactive::before {
display: none;
}
.sidebar-nav-item--nested.sidebar-nav-item--inactive {
font-weight: var(--font-weight-normal);
}
.sidebar-nav-item--hidden,
.sidebar-section--hidden {
display: none !important;
}
.sidebar-version-warning {
background-color: var(--color-bg-warning, #fff3cd);
border: 1px solid var(--color-border-warning, #ffc107);
border-radius: var(--radius-base);
padding: var(--space-3);
margin-bottom: var(--space-4);
p {
color: var(--color-text-primary);
line-height: 1.5;
}
p + p {
margin-top: var(--space-2);
}
a {
color: var(--color-text-primary);
text-decoration: underline;
font-weight: var(--font-weight-semibold);
&:hover {
color: var(--color-text-link);
}
}
}
.sidebar-nav-icon {
padding: var(--space-2);
border-radius: var(--radius-base);

View File

@@ -71,6 +71,15 @@ export class SidebarController {
this.initialActiveSubmenuPath = [...this.activeSubmenuPath];
this.versionManager?.updatePath(this.activeSubmenuPath);
// Check version compatibility for the active submenu
const activeSubmenuId = this.activeSubmenuPath[this.activeSubmenuPath.length - 1];
const activeSubmenuColumn = this.sidebar?.querySelector(
`[data-parent-id="${activeSubmenuId}"]`
) as HTMLElement;
if (activeSubmenuColumn) {
this.checkVersionCompatibility(activeSubmenuColumn);
}
if (this.navContainer) {
this.navContainer.dataset.currentNavLevel = String(initialActiveLevel);
}
@@ -117,6 +126,9 @@ export class SidebarController {
this.activeLevel = level;
this.versionManager?.updatePath(this.activeSubmenuPath);
// Check version compatibility for this submenu
this.checkVersionCompatibility(submenuColumn);
this.updateActiveColumns();
if (this.navContainer) {
@@ -129,6 +141,41 @@ export class SidebarController {
}, TRANSITION_DURATION);
}
private checkVersionCompatibility(submenuColumn: HTMLElement): void {
const supportedVersions = submenuColumn.dataset.supportedVersions?.split(',') || [];
const currentMenuVersion = this.versionManager?.getVersion() || '';
const urlVersion = this.versionManager?.getUrlVersion() || '';
const warningElement = submenuColumn.querySelector('[data-version-warning]') as HTMLElement;
if (!warningElement) return;
// If no versions are supported (non-versioned submenu), hide warning
if (supportedVersions.length === 0 || supportedVersions[0] === '') {
warningElement.style.display = 'none';
return;
}
// If current menu version is not supported by this submenu
if (!supportedVersions.includes(currentMenuVersion)) {
// Auto-revert to URL version if supported, otherwise to latest version
const targetVersion = supportedVersions.includes(urlVersion)
? urlVersion
: supportedVersions[0]; // First version is the latest
// Only show warning if we're switching from v3
if (currentMenuVersion === '3x') {
warningElement.style.display = 'block';
} else {
warningElement.style.display = 'none';
}
// Auto-switch to compatible version
this.versionManager?.setVersion(targetVersion);
} else {
warningElement.style.display = 'none';
}
}
private navigateBack(): void {
if (this.activeSubmenuPath.length <= 1) return;

View File

@@ -1,7 +1,7 @@
---
import type { MenuItem, VersionPrefix } from '@/config/types';
import SidebarNavItem from './SidebarNavItem.astro';
import { isLink, hasSubmenu, filterItems, getItemId, resolveHref, normalizePath } from './utils';
import { isLink, hasSubmenu, getItemId, resolveHref, normalizePath } from './utils';
interface Props {
items: MenuItem[];
@@ -31,7 +31,8 @@ const {
targetLevel,
} = Astro.props;
const filteredItems = filterItems(items, version);
// Don't filter items server-side - render all items and filter client-side based on selected version
const filteredItems = items;
---
{

View File

@@ -1,12 +1,11 @@
---
import type { Menu, VersionPrefix } from '@/config/types';
import type { VersionConfig } from '@/components/patterns/VersionSwitcher/types';
import { Body } from '@/components/primitives';
import { Body, BodyMd } from '@/components/primitives';
import { Icon } from 'astro-icon/components';
import { VersionSwitcher } from '@/components/patterns';
import SidebarItemsList from './SidebarItemsList.astro';
import {
shouldOmitSection,
collectAllSubmenus,
submenuContainsCurrentPath,
calculateInitialActiveLevel,
@@ -63,28 +62,26 @@ const initialActiveLevel =
>
<nav class="sidebar-nav" aria-label="Main navigation">
<ul class="sidebar-nav-list">
{menu.sections
?.filter((section) => !shouldOmitSection(section, version))
.map((section, sectionIndex) => (
<li class="sidebar-section">
{section.title && <Body as="h3">{section.title}</Body>}
<ul class="sidebar-section-list">
<SidebarItemsList
items={section.items}
parentId={parentId}
sectionIndex={sectionIndex}
currentPath={currentPath}
lang={lang}
basePath={basePath}
versioned={versioned}
version={version}
variant="root"
showIcon={true}
targetLevel={level + 1}
/>
</ul>
</li>
))}
{menu.sections?.map((section, sectionIndex) => (
<li class="sidebar-section" data-omit-from={section.omitFrom?.join(',') || undefined}>
{section.title && <Body as="h3">{section.title}</Body>}
<ul class="sidebar-section-list">
<SidebarItemsList
items={section.items}
parentId={parentId}
sectionIndex={sectionIndex}
currentPath={currentPath}
lang={lang}
basePath={basePath}
versioned={versioned}
version={version}
variant="root"
showIcon={true}
targetLevel={level + 1}
/>
</ul>
</li>
))}
<SidebarItemsList
items={menu.items ?? []}
parentId={parentId}
@@ -133,6 +130,7 @@ const initialActiveLevel =
panelHasActiveItem && 'sidebar-nav-panel--active',
]}
data-parent-id={submenu.id}
data-supported-versions={availableVersions.map((v) => v.id).join(',')}
aria-hidden="true"
>
{availableVersions.length > 0 && (
@@ -145,6 +143,20 @@ const initialActiveLevel =
data-version-switcher
/>
)}
<div
class="sidebar-version-warning"
data-version-warning
aria-live="polite"
style="display: none;"
>
<BodyMd vMargin={false}>
<strong>Note:</strong> There is no documentation available for v3.x.
</BodyMd>
<BodyMd vMargin={false}>
Express v3 has reached end-of-life. For support information, visit the{' '}
<a href={`/${lang}/support`}>support page</a>.
</BodyMd>
</div>
<nav class="sidebar-nav" aria-label={`${submenu.title} navigation`}>
<button
class="sidebar-nav-back"
@@ -164,32 +176,33 @@ const initialActiveLevel =
</Body>
</button>
<div class="sidebar-nav-content">
{submenu.menu.sections
?.filter((section) => !shouldOmitSection(section, version))
.map((section, sectionIndex) => (
<div class="sidebar-section">
{section.title && (
<Body as="h3" weight="bold" aria-label={`Section: ${section.title}`}>
{section.title}
</Body>
)}
<ul class="sidebar-section-list">
<SidebarItemsList
items={section.items}
parentId={submenu.id}
sectionIndex={sectionIndex}
currentPath={currentPath}
lang={lang}
basePath={submenu.basePath}
versioned={submenu.versioned}
version={version}
variant="nested"
showIcon={false}
targetLevel={navLevel + 1}
/>
</ul>
</div>
))}
{submenu.menu.sections?.map((section, sectionIndex) => (
<div
class="sidebar-section"
data-omit-from={section.omitFrom?.join(',') || undefined}
>
{section.title && (
<Body as="h3" weight="bold" aria-label={`Section: ${section.title}`}>
{section.title}
</Body>
)}
<ul class="sidebar-section-list">
<SidebarItemsList
items={section.items}
parentId={submenu.id}
sectionIndex={sectionIndex}
currentPath={currentPath}
lang={lang}
basePath={submenu.basePath}
versioned={submenu.versioned}
version={version}
variant="nested"
showIcon={false}
targetLevel={navLevel + 1}
/>
</ul>
</div>
))}
{submenu.menu.items && submenu.menu.items.length > 0 && (
<ul class="sidebar-section-list">
<SidebarItemsList

View File

@@ -65,6 +65,7 @@ const {
data-submenu-trigger
data-target-id={submenuId}
data-target-level={targetLevel}
data-omit-from={item.omitFrom?.join(',') || undefined}
>
{showIcon && item.icon && (
<div class="sidebar-nav-icon">

View File

@@ -1,11 +1,13 @@
export class SidebarVersionManager {
private sidebar: HTMLElement;
private currentVersion: string;
private urlVersion: string; // Version from the URL (content version)
private menuVersion: string; // Version selected in the menu switcher
private activeSubmenuPath: string[];
constructor(sidebar: HTMLElement, currentVersion: string, activeSubmenuPath: string[]) {
this.sidebar = sidebar;
this.currentVersion = currentVersion;
this.urlVersion = currentVersion;
this.menuVersion = currentVersion;
this.activeSubmenuPath = activeSubmenuPath;
}
@@ -20,11 +22,45 @@ export class SidebarVersionManager {
this.handleVersionChange(select.value);
});
});
// Listen for in-page link clicks to revert menu to URL version
this.setupInPageLinkListener();
// Initialize menu visibility for current URL version
this.updateMenuForVersion(this.urlVersion);
}
private setupInPageLinkListener(): void {
// Listen for clicks on any links in the main content area
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement;
const link = target.closest('a');
// Check if it's a link and not in the sidebar
if (link && !this.sidebar.contains(link)) {
const href = link.getAttribute('href');
// If it's an internal link (not external, not hash-only)
if (href && !href.startsWith('http') && !href.startsWith('#')) {
// Revert menu to URL version when clicking internal links
this.revertToUrlVersion();
}
}
});
}
handleVersionChange(newVersion: string): void {
const previousVersion = this.currentVersion;
this.currentVersion = newVersion;
const previousVersion = this.menuVersion;
this.menuVersion = newVersion;
// Sync all version switchers to the same value
const versionSelects =
this.sidebar.querySelectorAll<HTMLSelectElement>('[data-version-select]');
versionSelects.forEach((select) => {
if (select.value !== newVersion) {
select.value = newVersion;
}
});
document.dispatchEvent(
new CustomEvent('sidebar:versionChange', {
@@ -32,82 +68,89 @@ export class SidebarVersionManager {
})
);
const currentPath = this.sidebar.dataset.currentPath || '';
// Update menu visibility based on the new version
this.updateMenuForVersion(newVersion);
if (currentPath.includes(`/${previousVersion}/`)) {
if (this.isPathOmittedForVersion(currentPath, newVersion)) {
const fallbackPath = this.getFirstAvailableLinkForVersion(newVersion, previousVersion);
if (fallbackPath) {
window.location.href = fallbackPath;
return;
}
// Update link hrefs to point to the new version
this.updateLinkVersions(previousVersion, newVersion);
// Update active states - remove them if menu version doesn't match URL version
this.updateActiveStates();
}
private updateMenuForVersion(version: string): void {
// Show/hide menu items based on omitFrom attribute
const allMenuItems = this.sidebar.querySelectorAll('[data-omit-from]');
allMenuItems.forEach((item) => {
const omitFrom = (item as HTMLElement).dataset.omitFrom?.split(',') || [];
const shouldHide = omitFrom.includes(version);
if (shouldHide) {
item.classList.add('sidebar-nav-item--hidden');
item.setAttribute('aria-hidden', 'true');
} else {
item.classList.remove('sidebar-nav-item--hidden');
item.setAttribute('aria-hidden', 'false');
}
});
window.location.href = currentPath.replace(`/${previousVersion}/`, `/${newVersion}/`);
return;
}
// Show/hide sections based on omitFrom attribute
const allSections = this.sidebar.querySelectorAll('.sidebar-section[data-omit-from]');
if (this.isInVersionedSubmenu()) {
const fallbackPath = this.getFirstAvailableLinkForVersion(newVersion, previousVersion);
if (fallbackPath) {
window.location.href = fallbackPath;
allSections.forEach((section) => {
const omitFrom = (section as HTMLElement).dataset.omitFrom?.split(',') || [];
const shouldHide = omitFrom.includes(version);
if (shouldHide) {
section.classList.add('sidebar-section--hidden');
section.setAttribute('aria-hidden', 'true');
} else {
section.classList.remove('sidebar-section--hidden');
section.setAttribute('aria-hidden', 'false');
}
});
}
private updateLinkVersions(previousVersion: string, newVersion: string): void {
// Update all versioned links to point to the new version
const allLinks = this.sidebar.querySelectorAll<HTMLAnchorElement>('a.sidebar-nav-item[href]');
allLinks.forEach((link) => {
const href = link.getAttribute('href');
if (href && href.includes(`/${previousVersion}/`)) {
const newHref = href.replace(`/${previousVersion}/`, `/${newVersion}/`);
link.setAttribute('href', newHref);
}
});
}
private updateActiveStates(): void {
// If menu version doesn't match URL version, remove all active states
const allActiveItems = this.sidebar.querySelectorAll('.sidebar-nav-item--active');
if (this.menuVersion !== this.urlVersion) {
allActiveItems.forEach((item) => {
item.classList.add('sidebar-nav-item--inactive');
item.classList.remove('sidebar-nav-item--active');
});
} else {
allActiveItems.forEach((item) => {
item.classList.remove('sidebar-nav-item--inactive');
});
}
}
private isInVersionedSubmenu(): boolean {
const activeSubmenuId = this.activeSubmenuPath[this.activeSubmenuPath.length - 1];
if (activeSubmenuId === 'root') return false;
const activePanel = this.sidebar.querySelector(
`[data-parent-id="${activeSubmenuId}"]`
) as HTMLElement;
if (!activePanel) return false;
// Check if the active panel contains versioned links
const versionedLink = activePanel.querySelector('a[href*="/4x/"], a[href*="/5x/"]');
return versionedLink !== null;
}
private isPathOmittedForVersion(currentPath: string, targetVersion: string): boolean {
const activeLink = this.sidebar.querySelector(
'a.sidebar-nav-item--active[data-omit-from]'
) as HTMLAnchorElement;
if (!activeLink) return false;
const omitFrom = activeLink.dataset.omitFrom?.split(',') || [];
return omitFrom.includes(targetVersion);
}
private getFirstAvailableLinkForVersion(
targetVersion: string,
previousVersion: string
): string | null {
const activeSubmenuId = this.activeSubmenuPath[this.activeSubmenuPath.length - 1];
const activePanel = this.sidebar.querySelector(
`[data-parent-id="${activeSubmenuId}"]`
) as HTMLElement;
if (!activePanel) return null;
const links = activePanel.querySelectorAll(
'a.sidebar-nav-item[href]'
) as NodeListOf<HTMLAnchorElement>;
for (const link of links) {
const omitFrom = link.dataset.omitFrom?.split(',') || [];
if (!omitFrom.includes(targetVersion)) {
const href = link.getAttribute('href') || '';
if (href.includes(`/${previousVersion}/`)) {
return href.replace(`/${previousVersion}/`, `/${targetVersion}/`);
}
return href;
}
public revertToUrlVersion(): void {
// Revert menu back to URL version
if (this.menuVersion !== this.urlVersion) {
const versionSelects =
this.sidebar.querySelectorAll<HTMLSelectElement>('[data-version-select]');
versionSelects.forEach((select) => {
select.value = this.urlVersion;
});
this.handleVersionChange(this.urlVersion);
}
return null;
}
updateVisibility(activeLevel: number): void {
@@ -131,7 +174,11 @@ export class SidebarVersionManager {
}
getVersion(): string {
return this.currentVersion;
return this.menuVersion;
}
getUrlVersion(): string {
return this.urlVersion;
}
setVersion(version: string): void {