Skip to content
Merged
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
fix(a11y): add correct aria-label on ThemeToggle button (nodejs#8045
)

* fix aria-label a11y

* fix test

* add a11y test for aria-label

* fix i18n aria-label text

* fix hydration

* refactor test

* fix test

* Exclude ThemeToggle btn from SSR
  • Loading branch information
ShubhamOulkar authored Aug 12, 2025
commit 0293ad9e47f432e69852af2ee62cb819309db1cf
18 changes: 16 additions & 2 deletions apps/site/components/withNavBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import LanguageDropdown from '@node-core/ui-components/Common/LanguageDropDown';
import Skeleton from '@node-core/ui-components/Common/Skeleton';
import ThemeToggle from '@node-core/ui-components/Common/ThemeToggle';
import NavBar from '@node-core/ui-components/Containers/NavBar';
// TODO(@AvivKeller): I don't like that we are importing styles from another module
import styles from '@node-core/ui-components/Containers/NavBar/index.module.css';
Expand All @@ -27,6 +26,16 @@ const SearchButton = dynamic(() => import('#site/components/Common/Search'), {
),
});

const ThemeToggle = dynamic(
() => import('@node-core/ui-components/Common/ThemeToggle'),
{
ssr: false,
loading: () => (
<Skeleton className={styles.themeToggleSkeleton} loading={true} />
),
}
);

const WithNavBar: FC = () => {
const { navigationItems } = useSiteNavigation();
const { resolvedTheme, setTheme } = useTheme();
Expand All @@ -39,6 +48,11 @@ const WithNavBar: FC = () => {
const toggleCurrentTheme = () =>
setTheme(resolvedTheme === 'dark' ? 'light' : 'dark');

const themeToggleAriaLabel =
resolvedTheme === 'dark'
? t('components.common.themeToggle.light')
: t('components.common.themeToggle.dark');

const changeLanguage = (locale: SimpleLocaleConfig) =>
replace(pathname!, { locale: locale.code });

Expand All @@ -63,7 +77,7 @@ const WithNavBar: FC = () => {

<ThemeToggle
onClick={toggleCurrentTheme}
aria-label={t('components.common.themeToggle.label')}
aria-label={themeToggleAriaLabel}
/>

<LanguageDropdown
Expand Down
31 changes: 23 additions & 8 deletions apps/site/tests/e2e/general-behavior.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,25 @@ const locators = {
navLinksLocator: `[aria-label="${englishLocale.components.containers.navBar.controls.toggle}"] + div`,
// Global UI controls
languageDropdownName: englishLocale.components.common.languageDropdown.label,
themeToggleName: englishLocale.components.common.themeToggle.label,

themeToggleAriaLabels: {
light: englishLocale.components.common.themeToggle.light,
dark: englishLocale.components.common.themeToggle.dark,
},
// Search components (from Orama library)
searchButtonTag: 'orama-button',
searchInputTag: 'orama-input',
searchResultsTag: 'orama-search-results',
};

const getTheme = (page: Page) =>
page.evaluate(() => document.documentElement.dataset.theme);
page.evaluate(
() => document.documentElement.dataset.theme as 'light' | 'dark'
);

const getCurrentAriaLabel = (theme: string) =>
theme === 'dark'
? locators.themeToggleAriaLabels.light
: locators.themeToggleAriaLabels.dark;

const openLanguageMenu = async (page: Page) => {
const button = page.getByRole('button', {
Expand Down Expand Up @@ -65,21 +74,27 @@ test.describe('Node.js Website', () => {
test.describe('Theme', () => {
test('should toggle between light/dark themes', async ({ page }) => {
const themeToggle = page.getByRole('button', {
name: locators.themeToggleName,
name: /Switch to (Light|Dark) Mode/i,
});
await expect(themeToggle).toBeVisible();

const initialTheme = await getTheme(page);
const initialAriaLabel = getCurrentAriaLabel(initialTheme);
let currentAriaLabel = await themeToggle.getAttribute('aria-label');
expect(currentAriaLabel).toBe(initialAriaLabel);

await themeToggle.click();

const newTheme = await getTheme(page);
expect(newTheme).not.toEqual(initialTheme);
expect(['light', 'dark']).toContain(newTheme);
const newAriaLabel = getCurrentAriaLabel(newTheme);
currentAriaLabel = await themeToggle.getAttribute('aria-label');

expect(newTheme).not.toBe(initialTheme);
expect(currentAriaLabel).toBe(newAriaLabel);
});

test('should persist theme across page navigation', async ({ page }) => {
const themeToggle = page.getByRole('button', {
name: locators.themeToggleName,
name: /Switch to (Light|Dark) Mode/i,
});
await themeToggle.click();
const selectedTheme = await getTheme(page);
Expand Down
3 changes: 2 additions & 1 deletion packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,8 @@
"label": "Choose Language"
},
"themeToggle": {
"label": "Toggle Dark Mode"
"light": "Switch to Light Mode",
"dark": "Switch to Dark Mode"
}
},
"metabar": {
Expand Down
6 changes: 6 additions & 0 deletions packages/ui-components/src/Containers/NavBar/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,12 @@ span.searchButtonSkeleton {
}
}

span.themeToggleSkeleton {
@apply size-9
rounded-md
py-4;
}

.ghIconWrapper {
@apply size-9
rounded-md
Expand Down
Loading