From c43d2d8b0677ba6983d97aa12de31897132195df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rog=C3=A9rio=20Chaves?= Date: Sat, 8 Nov 2025 09:20:50 +0100 Subject: [PATCH] feat: synchronize CodeGroup tab selection across page and persist preference Adds state management to CodeGroup component to synchronize tab selection across all code groups on the same page and persist user preference to localStorage. When a user selects a language tab (e.g., TypeScript, JavaScript), all other code groups on the page automatically switch to the same language, and the preference is saved. When the user returns to the documentation, their preferred language is automatically selected. Key features: - Synchronizes tab selection across all CodeGroup instances on the page - Persists selection to localStorage for future visits - Cross-tab synchronization using storage events - Same-page synchronization using custom events - Graceful error handling for SSR and private browsing mode - Only switches to tabs that exist in each code group This significantly improves UX for users who consistently prefer one language, eliminating the need to manually switch tabs on every code example. --- src/app/components/mdx/CodeGroup.tsx | 71 +++++++++++++++++++++++++++- 1 file changed, 69 insertions(+), 2 deletions(-) diff --git a/src/app/components/mdx/CodeGroup.tsx b/src/app/components/mdx/CodeGroup.tsx index bd477746..cb308835 100644 --- a/src/app/components/mdx/CodeGroup.tsx +++ b/src/app/components/mdx/CodeGroup.tsx @@ -1,10 +1,13 @@ -import type { ReactElement } from 'react' +import { type ReactElement, useEffect, useState } from 'react' import * as Tabs from '../Tabs.js' import * as styles from './CodeGroup.css.js' +const STORAGE_KEY = 'codegroup-selected-tab' + export function CodeGroup({ children }: { children: ReactElement[] }) { if (!Array.isArray(children)) return null + const tabs = children.map((child_: any) => { const child = child_.props['data-title'] ? child_ : child_.props.children const { props } = child @@ -12,8 +15,72 @@ export function CodeGroup({ children }: { children: ReactElement[] }) { const content = props.children as ReactElement return { title, content } }) + + const tabValues = tabs.map((tab) => tab.title || '').filter(Boolean) + const [selectedTab, setSelectedTab] = useState(tabs[0]?.title || '') + + // Load from localStorage on mount and set up listener + useEffect(() => { + const loadFromStorage = () => { + try { + const stored = localStorage.getItem(STORAGE_KEY) + if (stored && tabValues.includes(stored)) { + setSelectedTab(stored) + } + } catch (error) { + // Handle localStorage access errors (e.g., in SSR or private browsing) + console.warn('Could not access localStorage for code group tab:', error) + } + } + + // Load initial value + loadFromStorage() + + // Listen for storage changes from other tabs/windows + const handleStorageChange = (e: StorageEvent) => { + if (e.key === STORAGE_KEY && e.newValue && tabValues.includes(e.newValue)) { + setSelectedTab(e.newValue) + } + } + + // Listen for custom storage events from same page + const handleCustomStorageChange = (e: CustomEvent) => { + const newValue = e.detail.value + if (newValue && tabValues.includes(newValue)) { + setSelectedTab(newValue) + } + } + + window.addEventListener('storage', handleStorageChange) + window.addEventListener('codegroup-storage-change', handleCustomStorageChange as EventListener) + + return () => { + window.removeEventListener('storage', handleStorageChange) + window.removeEventListener( + 'codegroup-storage-change', + handleCustomStorageChange as EventListener, + ) + } + }, [tabValues]) + + const handleTabChange = (value: string) => { + setSelectedTab(value) + + try { + localStorage.setItem(STORAGE_KEY, value) + // Dispatch custom event for same-page synchronization + window.dispatchEvent( + new CustomEvent('codegroup-storage-change', { + detail: { value }, + }), + ) + } catch (error) { + console.warn('Could not save code group tab to localStorage:', error) + } + } + return ( - + {tabs.map(({ title }, i) => (