diff --git a/components/Common/AvatarGroup/__tests__/index.test.mjs b/components/Common/AvatarGroup/__tests__/index.test.mjs
index 203f192dbcee2..7fded2f2265fd 100644
--- a/components/Common/AvatarGroup/__tests__/index.test.mjs
+++ b/components/Common/AvatarGroup/__tests__/index.test.mjs
@@ -26,7 +26,7 @@ const avatars = names.map(name => ({
alt: name,
}));
-describe('AvatarGroup component', () => {
+describe('AvatarGroup', () => {
it('renders the AvatarGroup component properly', () => {
const { getByText } = render();
diff --git a/components/Common/LanguageDropDown/index.module.css b/components/Common/LanguageDropDown/index.module.css
index c5dd3c9bd6e9e..cb76020aab1e5 100644
--- a/components/Common/LanguageDropDown/index.module.css
+++ b/components/Common/LanguageDropDown/index.module.css
@@ -1,12 +1,15 @@
-.iconWrapper {
+.languageDropdown {
@apply h-9
w-9
rounded-md
- bg-neutral-100
p-2
text-neutral-700
- dark:bg-neutral-900
dark:text-neutral-300;
+
+ &:hover {
+ @apply bg-neutral-100
+ dark:bg-neutral-900;
+ }
}
.dropDownContent {
diff --git a/components/Common/LanguageDropDown/index.tsx b/components/Common/LanguageDropDown/index.tsx
index 5c2f81bf6d063..b41b3f1b7ef6d 100644
--- a/components/Common/LanguageDropDown/index.tsx
+++ b/components/Common/LanguageDropDown/index.tsx
@@ -30,7 +30,7 @@ const LanguageDropdown: FC = ({
return (
-
diff --git a/components/Common/ThemeToggle/__tests__/index.test.mjs b/components/Common/ThemeToggle/__tests__/index.test.mjs
new file mode 100644
index 0000000000000..d283a51f53f82
--- /dev/null
+++ b/components/Common/ThemeToggle/__tests__/index.test.mjs
@@ -0,0 +1,38 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+
+import { LocaleProvider } from '@/providers/localeProvider';
+
+import ThemeToggle from '../';
+
+let mockCurrentTheme = 'light';
+
+const toggleTheme = () => {
+ mockCurrentTheme = mockCurrentTheme === 'light' ? 'dark' : 'light';
+};
+
+describe('ThemeToggle', () => {
+ let toggle;
+
+ beforeEach(() => {
+ mockCurrentTheme = 'light';
+
+ render(
+
+
+
+ );
+ toggle = screen.getByRole('button');
+ });
+
+ it('switches dark theme to light theme', async () => {
+ mockCurrentTheme = 'dark';
+ await userEvent.click(toggle);
+ expect(mockCurrentTheme).toBe('light');
+ });
+
+ it('switches light theme to dark theme', async () => {
+ await userEvent.click(toggle);
+ expect(mockCurrentTheme).toBe('dark');
+ });
+});
diff --git a/components/Common/ThemeToggle/index.module.css b/components/Common/ThemeToggle/index.module.css
new file mode 100644
index 0000000000000..8f95a0a9a4241
--- /dev/null
+++ b/components/Common/ThemeToggle/index.module.css
@@ -0,0 +1,13 @@
+.themeToggle {
+ @apply h-9
+ w-9
+ rounded-md
+ p-2
+ text-neutral-700
+ dark:text-neutral-300;
+
+ &:hover {
+ @apply bg-neutral-100
+ dark:bg-neutral-900;
+ }
+}
diff --git a/components/Common/ThemeToggle/index.stories.tsx b/components/Common/ThemeToggle/index.stories.tsx
new file mode 100644
index 0000000000000..d249c438179ed
--- /dev/null
+++ b/components/Common/ThemeToggle/index.stories.tsx
@@ -0,0 +1,10 @@
+import type { Meta as MetaObj, StoryObj } from '@storybook/react';
+
+import ThemeToggle from '@/components/Common/ThemeToggle';
+
+type Story = StoryObj;
+type Meta = MetaObj;
+
+export const Default: Story = {};
+
+export default { component: ThemeToggle } as Meta;
diff --git a/components/Common/ThemeToggle/index.tsx b/components/Common/ThemeToggle/index.tsx
new file mode 100644
index 0000000000000..bdba686edac88
--- /dev/null
+++ b/components/Common/ThemeToggle/index.tsx
@@ -0,0 +1,31 @@
+import { MoonIcon, SunIcon } from '@heroicons/react/24/outline';
+import type { FC, MouseEvent } from 'react';
+import { useIntl } from 'react-intl';
+
+import styles from './index.module.css';
+
+type ThemeToggleProps = {
+ onClick?: (event: MouseEvent) => void;
+};
+
+const ThemeToggle: FC = ({ onClick = () => {} }) => {
+ const { formatMessage } = useIntl();
+
+ const ariaLabel = formatMessage({
+ id: 'components.header.buttons.toggleDarkMode',
+ });
+
+ return (
+
+
+
+
+ );
+};
+
+export default ThemeToggle;
diff --git a/package-lock.json b/package-lock.json
index 5eabf2590993c..a175f85119b4d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"@heroicons/react": "~2.0.18",
"@mdx-js/react": "^2.3.0",
"@nodevu/core": "~0.1.0",
+ "@radix-ui/react-accessible-icon": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-select": "^2.0.0",
@@ -4438,6 +4439,29 @@
"@babel/runtime": "^7.13.10"
}
},
+ "node_modules/@radix-ui/react-accessible-icon": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.0.3.tgz",
+ "integrity": "sha512-duVGKeWPSUILr/MdlPxV+GeULTc2rS1aihGdQ3N2qCUPMgxYLxvAsHJM3mCVLF8d5eK+ympmB22mb1F3a5biNw==",
+ "dependencies": {
+ "@babel/runtime": "^7.13.10",
+ "@radix-ui/react-visually-hidden": "1.0.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0",
+ "react-dom": "^16.8 || ^17.0 || ^18.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-arrow": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz",
diff --git a/package.json b/package.json
index bf98b79b7c616..a7826c16c76ea 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
"@heroicons/react": "~2.0.18",
"@mdx-js/react": "^2.3.0",
"@nodevu/core": "~0.1.0",
+ "@radix-ui/react-accessible-icon": "^1.0.3",
"@radix-ui/react-avatar": "^1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-select": "^2.0.0",