Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/site/app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { getLocale } from 'next-intl/server';

import BaseLayout from '#site/layouts/Base';
import { IBM_PLEX_MONO, OPEN_SANS } from '#site/next.fonts';
import { HashProvider } from '#site/providers/hashProvider';
import { ThemeProvider } from '#site/providers/themeProvider';

import type { FC, PropsWithChildren } from 'react';
Expand All @@ -29,9 +30,11 @@ const RootLayout: FC<PropsWithChildren> = async ({ children }) => {
>
<body suppressHydrationWarning>
<NextIntlClientProvider>
<ThemeProvider>
<BaseLayout>{children}</BaseLayout>
</ThemeProvider>
<HashProvider>
<ThemeProvider>
<BaseLayout>{children}</BaseLayout>
</ThemeProvider>
</HashProvider>
</NextIntlClientProvider>

<a
Expand Down
45 changes: 45 additions & 0 deletions apps/site/hooks/useHash.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
'use client';

import { useSyncExternalStore } from 'react';

const listeners = new Set<() => void>();
let isListening = false;

const onHashChange = () => {
listeners.forEach(rerender => rerender());
};

const subscribe = (callback: () => void) => {
listeners.add(callback);
if (!isListening && typeof window !== 'undefined') {
window.addEventListener('hashchange', onHashChange);
isListening = true;
}
return () => {
listeners.delete(callback);
if (listeners.size === 0 && typeof window !== 'undefined') {
window.removeEventListener('hashchange', onHashChange);
isListening = false;
}
};
};

const getSnapshot = () =>
typeof window !== 'undefined' ? window.location.hash.slice(1) : '';
const getServerSnapshot = () => '';

const useHash = () => {
// Works in both client and server environments, provides the initial value for the store during server rendering and hydration avoiding hydration mismatches
const hash = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);

const setHash = (newHash: string) => {
if (typeof window !== 'undefined') {
window.history.replaceState(null, '', `#${newHash}`);
onHashChange();
}
};

return [hash, setHash] as const;
};

export default useHash;
13 changes: 13 additions & 0 deletions apps/site/providers/hashProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
'use client';

import { HashContext } from '@node-core/ui-components/contexts/HashContext';

import useHash from '#site/hooks/useHash';

import type { FC, PropsWithChildren } from 'react';

export const HashProvider: FC<PropsWithChildren> = ({ children }) => {
const [hash, setHash] = useHash();

return <HashContext value={{ hash, setHash }}>{children}</HashContext>;
};
2 changes: 1 addition & 1 deletion packages/ui-components/src/Common/CodeTabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styles from './index.module.css';

type CodeTabsProps = Pick<
ComponentProps<typeof Tabs>,
'tabs' | 'defaultValue' | 'children' | 'addons'
'tabs' | 'defaultValue' | 'value' | 'onValueChange' | 'children' | 'addons'
>;

const CodeTabs: FC<CodeTabsProps> = ({ ...props }) => (
Expand Down
2 changes: 2 additions & 0 deletions packages/ui-components/src/Common/Tabs/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import styles from './index.module.css';

type Tab = {
key: string;
id?: string;
label: string;
secondaryLabel?: string;
value?: string;
Expand Down Expand Up @@ -34,6 +35,7 @@ const Tabs: FC<PropsWithChildren<TabsProps>> = ({
{tabs.map(tab => (
<TabsPrimitive.Trigger
key={tab.key}
id={tab.id}
value={tab.value ?? tab.key}
className={classNames(styles.tabsTrigger, triggerClassName)}
>
Expand Down
52 changes: 52 additions & 0 deletions packages/ui-components/src/MDX/CodeTabs/CodeTabsWithHash.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
'use client';

import { useCallback, useState } from 'react';

import CodeTabs from '#ui/Common/CodeTabs';
import { useHashContext } from '#ui/contexts/HashContext';

import type { ComponentProps, FC } from 'react';

type CodeTabsWithHashProps = Omit<ComponentProps<typeof CodeTabs>, 'tabs'> & {
tabs: Array<{ key: string; label: string; id?: string }>;
};

const CodeTabsWithHash: FC<CodeTabsWithHashProps> = ({
tabs,
defaultValue,
...props
}) => {
const { hash, setHash } = useHashContext();
const [activeTab, setActiveTab] = useState(defaultValue);
const [prevHash, setPrevHash] = useState(hash);

if (hash !== prevHash) {
setPrevHash(hash);
const matched = tabs.find(t => t.id === hash);
if (matched && matched.key !== activeTab) {
setActiveTab(matched.key);
}
}

const handleValueChange = useCallback(
(value: string) => {
setActiveTab(value);
const matched = tabs.find(t => t.key === value);
if (matched?.id) {
setHash(matched.id);
}
},
[tabs, setHash]
);

return (
<CodeTabs
{...props}
tabs={tabs}
value={activeTab}
onValueChange={handleValueChange}
/>
);
};

export default CodeTabsWithHash;
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@ import CodeTabs from '#ui/Common/CodeTabs';

import type { FC, ReactElement } from 'react';

import CodeTabsWithHash from './CodeTabsWithHash';

type MDXCodeTabsProps = {
children: Array<ReactElement<unknown>>;
languages: string;
displayNames?: string;
defaultTab?: string;
groupId?: string;
};

const NAME_OVERRIDES: Record<string, string | undefined> = {
Expand All @@ -21,6 +24,7 @@ const MDXCodeTabs: FC<MDXCodeTabsProps> = ({
displayNames: rawDisplayNames,
children: codes,
defaultTab = '0',
groupId,
...props
}) => {
const { tabs, languages } = useMemo(() => {
Expand All @@ -44,14 +48,19 @@ const MDXCodeTabs: FC<MDXCodeTabsProps> = ({
return {
key: `${language}-${index}`,
label,
id:
groupId &&
`${groupId}-${language}-${index}`.replace(/[^a-zA-Z0-9-_]/g, '-'),
};
});

return { tabs, languages };
}, [rawLanguages, rawDisplayNames]);
}, [rawLanguages, rawDisplayNames, groupId]);

const Component = groupId ? CodeTabsWithHash : CodeTabs;

return (
<CodeTabs
<Component
tabs={tabs}
defaultValue={tabs[Number(defaultTab)].key}
{...props}
Expand All @@ -65,7 +74,7 @@ const MDXCodeTabs: FC<MDXCodeTabsProps> = ({
{codes[index]}
</TabsPrimitive.Content>
))}
</CodeTabs>
</Component>
);
};

Expand Down
15 changes: 15 additions & 0 deletions packages/ui-components/src/contexts/HashContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';

import { createContext, use } from 'react';

type HashContextType = {
hash: string;
setHash: (newHash: string) => void;
};

export const HashContext = createContext<HashContextType>({
hash: '',
setHash: () => {},
});

export const useHashContext = () => use(HashContext);