From 55de7aa0768cddc44d885c0e7ecc950ed66e45e5 Mon Sep 17 00:00:00 2001 From: Mohamed Shams El-Deen Date: Sun, 13 Sep 2026 20:01:11 +0300 Subject: [PATCH] feat(ui): implement deep linking in CodeTabs via URL hash --- .../src/Common/CodeTabs/index.tsx | 2 +- packages/ui-components/src/MDX/CodeTabs.tsx | 50 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/packages/ui-components/src/Common/CodeTabs/index.tsx b/packages/ui-components/src/Common/CodeTabs/index.tsx index 12ff05973037e..910fda8abfc58 100644 --- a/packages/ui-components/src/Common/CodeTabs/index.tsx +++ b/packages/ui-components/src/Common/CodeTabs/index.tsx @@ -6,7 +6,7 @@ import styles from './index.module.css'; type CodeTabsProps = Pick< ComponentProps, - 'tabs' | 'defaultValue' | 'children' | 'addons' + 'tabs' | 'defaultValue' | 'value' | 'onValueChange' | 'children' | 'addons' >; const CodeTabs: FC = ({ ...props }) => ( diff --git a/packages/ui-components/src/MDX/CodeTabs.tsx b/packages/ui-components/src/MDX/CodeTabs.tsx index a4092ae6ece73..ccb1bacade6f8 100644 --- a/packages/ui-components/src/MDX/CodeTabs.tsx +++ b/packages/ui-components/src/MDX/CodeTabs.tsx @@ -1,5 +1,7 @@ +'use client'; + import * as TabsPrimitive from '@radix-ui/react-tabs'; -import { useMemo } from 'react'; +import { useEffect, useId, useMemo, useState } from 'react'; import CodeTabs from '#ui/Common/CodeTabs'; @@ -10,6 +12,7 @@ type MDXCodeTabsProps = { languages: string; displayNames?: string; defaultTab?: string; + groupId?: string; }; const NAME_OVERRIDES: Record = { @@ -21,8 +24,12 @@ const MDXCodeTabs: FC = ({ displayNames: rawDisplayNames, children: codes, defaultTab = '0', + groupId, ...props }) => { + const id = useId(); + const prefix = groupId ? `tab-${groupId}` : `tab-${id}`; + const { tabs, languages } = useMemo(() => { const occurrences: Record = {}; @@ -43,17 +50,54 @@ const MDXCodeTabs: FC = ({ return { key: `${language}-${index}`, + anchorId: `${prefix}-${language}-${index}`.replace( + /[^a-zA-Z0-9-_]/g, + '-' + ), label, }; }); return { tabs, languages }; - }, [rawLanguages, rawDisplayNames]); + }, [rawLanguages, rawDisplayNames, prefix]); + + const [activeTab, setActiveTab] = useState(() => { + if (typeof window !== 'undefined' && window.location.hash) { + const hash = window.location.hash.slice(1); + const matched = tabs.find(t => t.anchorId === hash); + if (matched) { + return matched.key; + } + } + return tabs[Number(defaultTab)]?.key ?? tabs[0].key; + }); + + useEffect(() => { + const handleHashChange = () => { + const hash = window.location.hash.slice(1); + const matched = tabs.find(t => t.anchorId === hash); + if (matched) { + setActiveTab(matched.key); + } + }; + + window.addEventListener('hashchange', handleHashChange); + return () => window.removeEventListener('hashchange', handleHashChange); + }, [tabs]); + + const handleValueChange = (value: string) => { + setActiveTab(value); + const matched = tabs.find(t => t.key === value); + if (matched) { + window.history.replaceState(null, '', `#${matched.anchorId}`); + } + }; return ( {languages.map((_, index) => (