) => {
+ if (e.key === 'Enter' || e.key === ' ') {
+ item.onClick();
+ }
+ };
+
+ return (
+
+
+
+ );
+ });
+
+ const dropdownStyles = { display: shouldShow ? 'block' : 'none', ...css };
+
+ return (
+
+ );
+};
+
+export default Dropdown;
diff --git a/components/Common/LanguageSelector/__tests__/index.test.tsx b/components/Common/LanguageSelector/__tests__/index.test.tsx
new file mode 100644
index 0000000000000..28901cb0a7b32
--- /dev/null
+++ b/components/Common/LanguageSelector/__tests__/index.test.tsx
@@ -0,0 +1,34 @@
+import { fireEvent, render, screen } from '@testing-library/react';
+import LanguageSelector from '..';
+
+jest.mock('../../../../hooks/useLocale', () => ({
+ useLocale: () => ({
+ availableLocales: [
+ { code: 'en', name: 'English', localName: 'English' },
+ { code: 'es', name: 'Spanish', localName: 'EspaƱol' },
+ ],
+ currentLocale: { code: 'en', name: 'English', localName: 'English' },
+ }),
+}));
+
+describe('LanguageSelector', () => {
+ test('clicking the language switch button toggles the dropdown display', () => {
+ render();
+ const button = screen.getByRole('button');
+ expect(screen.queryByText('English')).not.toBeVisible();
+ fireEvent.click(button);
+ expect(screen.queryByText('English')).toBeVisible();
+ fireEvent.click(button);
+ expect(screen.queryByText('English')).not.toBeVisible();
+ });
+
+ test('renders the Dropdown component with correct style', () => {
+ render();
+ const button = screen.getByRole('button');
+ fireEvent.click(button);
+ const dropdown = screen.getByRole('list');
+ expect(dropdown).toHaveStyle(
+ 'position: absolute; top: 60%; right: 0; margin: 0;'
+ );
+ });
+});
diff --git a/components/Common/LanguageSelector/index.module.scss b/components/Common/LanguageSelector/index.module.scss
new file mode 100644
index 0000000000000..0db848421aad3
--- /dev/null
+++ b/components/Common/LanguageSelector/index.module.scss
@@ -0,0 +1,13 @@
+.languageSwitch {
+ background: none;
+ border: none;
+ color: var(--color-text-accent);
+ cursor: pointer;
+ line-height: 0;
+ padding: 0;
+}
+
+.container {
+ display: inline;
+ position: relative;
+}
diff --git a/components/Common/LanguageSelector/index.stories.tsx b/components/Common/LanguageSelector/index.stories.tsx
new file mode 100644
index 0000000000000..305b2209712b9
--- /dev/null
+++ b/components/Common/LanguageSelector/index.stories.tsx
@@ -0,0 +1,14 @@
+import LanguageSelector from '.';
+export default { component: LanguageSelector };
+
+export const Default = () => (
+
+
+
+);
diff --git a/components/Common/LanguageSelector/index.tsx b/components/Common/LanguageSelector/index.tsx
new file mode 100644
index 0000000000000..5a0493bad515e
--- /dev/null
+++ b/components/Common/LanguageSelector/index.tsx
@@ -0,0 +1,52 @@
+import { useMemo, useState } from 'react';
+import { MdOutlineTranslate } from 'react-icons/md';
+import { useLocale } from '../../../hooks/useLocale';
+import Dropdown from '../Dropdown';
+import styles from './index.module.scss';
+
+const dropdownStyle = {
+ position: 'absolute',
+ top: '60%',
+ right: '0',
+ margin: 0,
+};
+
+const LanguageSelector = () => {
+ const [showDropdown, setShowDropdown] = useState(false);
+
+ const { availableLocales, currentLocale } = useLocale();
+
+ const dropdownItems = useMemo(
+ () =>
+ availableLocales.map(locale => ({
+ title: locale.localName,
+ label: locale.name,
+ onClick: () => {
+ // TODO: "locale changing logic yet to be implemented"
+ },
+ active: currentLocale.code === locale.code,
+ })),
+ [availableLocales, currentLocale]
+ );
+
+ return (
+
+
+
+
+ );
+};
+
+export default LanguageSelector;
diff --git a/package-lock.json b/package-lock.json
index 96276218db863..49e84a61eba9c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -23,6 +23,7 @@
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-icons": "^4.8.0",
"react-intl": "^6.3.2",
"remark-gfm": "^3.0.1",
"sass": "^1.60.0",
@@ -20313,6 +20314,14 @@
"integrity": "sha512-Fl7FuabXsJnV5Q1qIOQwx/sagGF18kogb4gpfcG4gjLBWO0WDiiz1ko/ExayuxE7InyQkBLkxRFG5oxY6Uu3Kg==",
"dev": true
},
+ "node_modules/react-icons": {
+ "version": "4.8.0",
+ "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-4.8.0.tgz",
+ "integrity": "sha512-N6+kOLcihDiAnj5Czu637waJqSnwlMNROzVZMhfX68V/9bu9qHaMIJC4UdozWoOk57gahFCNHwVvWzm0MTzRjg==",
+ "peerDependencies": {
+ "react": "*"
+ }
+ },
"node_modules/react-inspector": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/react-inspector/-/react-inspector-6.0.1.tgz",
diff --git a/package.json b/package.json
index 85ae5ec5aa8a6..562cd7f261019 100644
--- a/package.json
+++ b/package.json
@@ -48,6 +48,7 @@
"npm-run-all": "^4.1.5",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "react-icons": "^4.8.0",
"react-intl": "^6.3.2",
"remark-gfm": "^3.0.1",
"sass": "^1.60.0",
diff --git a/types/dropdown.ts b/types/dropdown.ts
new file mode 100644
index 0000000000000..52bc847a4a9de
--- /dev/null
+++ b/types/dropdown.ts
@@ -0,0 +1,6 @@
+export interface DropdownItem {
+ title: string;
+ label: string;
+ onClick: () => void;
+ active?: boolean;
+}
diff --git a/types/index.ts b/types/index.ts
index 885a99dddac52..517723f71abd6 100644
--- a/types/index.ts
+++ b/types/index.ts
@@ -1,18 +1,19 @@
// @TODO: These types will be splitted on individual files for better organisation in the future
import type { AppProps as DefaultAppProps } from 'next/app';
+import type { BlogData } from './blog';
import type { LocaleContext } from './i18n';
import type { NodeVersionData } from './nodeVersions';
-import type { BlogData } from './blog';
+export * from './blog';
export * from './config';
-export * from './frontmatter';
+export * from './dropdown';
export * from './features';
+export * from './frontmatter';
+export * from './i18n';
export * from './layouts';
export * from './navigation';
export * from './nodeVersions';
-export * from './blog';
-export * from './i18n';
export interface AppProps {
i18nData: Pick;