+41
components/Common/ActiveLink/__tests__/index.test.mjs
+41
components/Common/ActiveLink/__tests__/index.test.mjs
···
1
+
import { render, screen } from '@testing-library/react';
2
+
3
+
import ActiveLink from '..';
4
+
5
+
describe('ActiveLink', () => {
6
+
it('renders as localized link', () => {
7
+
render(
8
+
<ActiveLink className="link" activeClassName="active" href="/link">
9
+
Link
10
+
</ActiveLink>
11
+
);
12
+
13
+
expect(screen.findByText('Link')).resolves.toHaveAttribute(
14
+
'href',
15
+
'/en/link'
16
+
);
17
+
});
18
+
19
+
it('ignores active class when href not matches location.href', () => {
20
+
render(
21
+
<ActiveLink className="link" activeClassName="active" href="/not-link">
22
+
Link
23
+
</ActiveLink>
24
+
);
25
+
26
+
expect(screen.findByText('Link')).resolves.toHaveAttribute('class', 'link');
27
+
});
28
+
29
+
it('sets active class when href matches location.href', () => {
30
+
render(
31
+
<ActiveLink className="link" activeClassName="active" href="/link">
32
+
Link
33
+
</ActiveLink>
34
+
);
35
+
36
+
expect(screen.findByText('Link')).resolves.toHaveAttribute(
37
+
'class',
38
+
'link active'
39
+
);
40
+
});
41
+
});
-53
components/Common/ActiveLocalizedLink/__tests__/index.test.mjs
-53
components/Common/ActiveLocalizedLink/__tests__/index.test.mjs
···
1
-
import { render, screen } from '@testing-library/react';
2
-
3
-
import ActiveLocalizedLink from '..';
4
-
5
-
describe('ActiveLocalizedLink', () => {
6
-
it('renders as localized link', () => {
7
-
render(
8
-
<ActiveLocalizedLink
9
-
className="link"
10
-
activeClassName="active"
11
-
href="/link"
12
-
>
13
-
Link
14
-
</ActiveLocalizedLink>
15
-
);
16
-
17
-
expect(screen.findByText('Link')).resolves.toHaveAttribute(
18
-
'href',
19
-
'/en/link'
20
-
);
21
-
});
22
-
23
-
it('ignores active class when href not matches location.href', () => {
24
-
render(
25
-
<ActiveLocalizedLink
26
-
className="link"
27
-
activeClassName="active"
28
-
href="/not-link"
29
-
>
30
-
Link
31
-
</ActiveLocalizedLink>
32
-
);
33
-
34
-
expect(screen.findByText('Link')).resolves.toHaveAttribute('class', 'link');
35
-
});
36
-
37
-
it('sets active class when href matches location.href', () => {
38
-
render(
39
-
<ActiveLocalizedLink
40
-
className="link"
41
-
activeClassName="active"
42
-
href="/link"
43
-
>
44
-
Link
45
-
</ActiveLocalizedLink>
46
-
);
47
-
48
-
expect(screen.findByText('Link')).resolves.toHaveAttribute(
49
-
'class',
50
-
'link active'
51
-
);
52
-
});
53
-
});
+9
-7
components/Common/ActiveLocalizedLink/index.tsx
components/Common/ActiveLink/index.tsx
+9
-7
components/Common/ActiveLocalizedLink/index.tsx
components/Common/ActiveLink/index.tsx
···
7
7
import { usePathname } from '@/navigation.mjs';
8
8
9
9
type ActiveLocalizedLinkProps = ComponentProps<typeof Link> & {
10
-
activeClassName: string;
10
+
activeClassName?: string;
11
+
allowSubPath?: boolean;
11
12
};
12
13
13
-
const ActiveLocalizedLink: FC<ActiveLocalizedLinkProps> = ({
14
+
const ActiveLink: FC<ActiveLocalizedLinkProps> = ({
14
15
children,
15
-
activeClassName,
16
+
activeClassName = 'active',
17
+
allowSubPath = false,
16
18
className,
17
19
href = '',
18
20
...props
19
21
}) => {
20
22
const pathname = usePathname();
21
23
22
-
const linkURL = new URL(href.toString(), location.href);
23
-
24
24
const finalClassName = classNames(className, {
25
-
[activeClassName]: linkURL.pathname === pathname,
25
+
[activeClassName]: allowSubPath
26
+
? pathname.startsWith(href.toString())
27
+
: href.toString() === pathname,
26
28
});
27
29
28
30
return (
···
32
34
);
33
35
};
34
36
35
-
export default ActiveLocalizedLink;
37
+
export default ActiveLink;
+6
-8
components/Header.tsx
+6
-8
components/Header.tsx
···
1
1
'use client';
2
2
3
-
import classNames from 'classnames';
4
3
import Image from 'next/image';
5
4
import { useTranslations } from 'next-intl';
6
5
import { useTheme } from 'next-themes';
7
6
import { useState } from 'react';
8
7
8
+
import ActiveLink from '@/components/Common/ActiveLink';
9
9
import Link from '@/components/Link';
10
-
import { useIsCurrentPathname, useSiteNavigation } from '@/hooks';
10
+
import { useSiteNavigation } from '@/hooks';
11
11
import { usePathname } from '@/navigation.mjs';
12
12
import { BASE_PATH } from '@/next.constants.mjs';
13
13
import { availableLocales } from '@/next.locales.mjs';
14
14
15
15
const Header = () => {
16
16
const { navigationItems } = useSiteNavigation();
17
-
const { isCurrentLocaleRoute } = useIsCurrentPathname();
18
17
const [showLangPicker, setShowLangPicker] = useState(false);
19
18
const { theme, setTheme } = useTheme();
20
19
21
20
const pathname = usePathname();
22
21
const t = useTranslations();
23
22
24
-
const getLinkClassName = (href: string) =>
25
-
classNames({ active: isCurrentLocaleRoute(href, href !== '/') });
26
-
27
23
const toggleLanguage = t('components.header.buttons.toggleLanguage');
28
24
const toggleDarkMode = t('components.header.buttons.toggleDarkMode');
29
25
···
43
39
<nav aria-label="primary">
44
40
<ul className="list-divider-pipe">
45
41
{navigationItems.map((item, key) => (
46
-
<li key={key} className={getLinkClassName(item.link)}>
47
-
<Link href={item.link}>{item.text}</Link>
42
+
<li key={key}>
43
+
<ActiveLink href={item.link} allowSubPath>
44
+
{item.text}
45
+
</ActiveLink>
48
46
</li>
49
47
))}
50
48
</ul>
-1
hooks/react-client/index.ts
-1
hooks/react-client/index.ts
···
1
1
export { default as useCopyToClipboard } from './useCopyToClipboard';
2
2
export { default as useDetectOS } from './useDetectOS';
3
-
export { default as useIsCurrentPathname } from './useIsCurrentPathname';
4
3
export { default as useMediaQuery } from './useMediaQuery';
5
4
export { default as useNotification } from './useNotification';
6
5
export { default as useClientContext } from './useClientContext';
-14
hooks/react-client/useIsCurrentPathname.ts
-14
hooks/react-client/useIsCurrentPathname.ts
···
1
-
'use client';
2
-
3
-
import useClientContext from '@/hooks/react-client/useClientContext';
4
-
5
-
const useIsCurrentPathname = () => {
6
-
const { pathname } = useClientContext();
7
-
8
-
return {
9
-
isCurrentLocaleRoute: (route: string, allowSubPath?: boolean) =>
10
-
allowSubPath ? pathname.startsWith(route) : route === pathname,
11
-
};
12
-
};
13
-
14
-
export default useIsCurrentPathname;
-1
hooks/react-server/index.ts
-1
hooks/react-server/index.ts
···
1
1
export { default as useCopyToClipboard } from './useCopyToClipboard';
2
2
export { default as useDetectOS } from './useDetectOS';
3
-
export { default as useIsCurrentPathname } from './useIsCurrentPathname';
4
3
export { default as useMediaQuery } from './useMediaQuery';
5
4
export { default as useNotification } from './useNotification';
6
5
export { default as useClientContext } from './useClientContext';
-17
hooks/react-server/useIsCurrentPathname.ts
-17
hooks/react-server/useIsCurrentPathname.ts
···
1
-
import useClientContext from '@/hooks/react-server/useClientContext';
2
-
3
-
const useIsCurrentPathname = () => {
4
-
const { pathname } = useClientContext();
5
-
6
-
return {
7
-
isCurrentLocaleRoute: (route: string, allowSubPath?: boolean) => {
8
-
const asPathJustPath = pathname.replace(/[#|?].*$/, '');
9
-
10
-
return allowSubPath
11
-
? asPathJustPath.startsWith(route)
12
-
: route === asPathJustPath;
13
-
},
14
-
};
15
-
};
16
-
17
-
export default useIsCurrentPathname;
-2
layouts/DocsLayout.tsx
-2
layouts/DocsLayout.tsx
+2
-4
styles/old/index.css
+2
-4
styles/old/index.css
+5
-4
styles/old/layout/dark-theme.css
+5
-4
styles/old/layout/dark-theme.css
+2
-2
styles/old/page-modules/header.css
+2
-2
styles/old/page-modules/header.css
···
142
142
143
143
$border-width: 14px;
144
144
145
-
&.active::after {
145
+
&:has(a.active)::after {
146
146
border: solid transparent;
147
147
border-top-color: $node-gray;
148
148
border-width: $border-width;
···
156
156
width: 0;
157
157
}
158
158
159
-
&.active:first-child::after {
159
+
&:has(a.active):first-child::after {
160
160
margin-left: -$border-width;
161
161
}
162
162
}
+4
-4
util/getBitness.ts
+4
-4
util/getBitness.ts
···
9
9
'bitness',
10
10
]);
11
11
12
-
// Apparently in some cases this is not a Promise, then we should ignore
13
-
if (entropyValues instanceof Promise) {
14
-
return entropyValues.then(ua => ua.bitness);
15
-
}
12
+
// Apparently in some cases this is not a Promise, we can Promisify it.
13
+
return Promise.resolve(entropyValues)
14
+
.then(({ bitness }) => bitness)
15
+
.catch(() => undefined);
16
16
}
17
17
18
18
return undefined;