revert(client): ensure donate button is always visible (#66754)

This commit is contained in:
Mrugesh Mohapatra
2026-04-02 22:36:25 +05:30
committed by GitHub
parent 72ddb7a992
commit a72fe07399
5 changed files with 55 additions and 196 deletions

View File

@@ -13,6 +13,7 @@ import { openSignoutModal, toggleTheme } from '../../../redux/actions';
import { Link } from '../../helpers';
import { LocalStorageThemes } from '../../../redux/types';
import { themeSelector } from '../../../redux/selectors';
import SupporterBadge from '../../../assets/icons/supporter-badge';
export interface NavLinksProps {
displayMenu: boolean;
@@ -35,6 +36,42 @@ const mapStateToProps = createSelector(
(theme: LocalStorageThemes) => ({ theme })
);
interface DonateButtonProps {
isUserDonating: boolean | undefined;
handleMenuKeyDown: (event: React.KeyboardEvent<HTMLAnchorElement>) => void;
}
const DonateButton = ({
isUserDonating,
handleMenuKeyDown
}: DonateButtonProps) => {
const { t } = useTranslation();
return (
<li key={isUserDonating ? 'supporter' : 'donate'}>
<Link
className={`nav-link nav-link-flex nav-link-header ${
isUserDonating && 'nav-link-supporter'
}`}
onKeyDown={handleMenuKeyDown}
sameTab={false}
to={isUserDonating ? '/supporters' : '/donate'}
data-test-label={
isUserDonating ? 'dropdown-support-button' : 'dropdown-donate-button'
}
>
{isUserDonating ? (
<>
{t('buttons.supporters')}
<SupporterBadge />
</>
) : (
<>{t('buttons.donate')}</>
)}
</Link>
</li>
);
};
function NavLinks({
menuButtonRef,
openSignoutModal,
@@ -45,7 +82,7 @@ function NavLinks({
toggleTheme
}: NavLinksProps) {
const { t } = useTranslation();
const { username: currentUserName } = user || {};
const { isDonating: isUserDonating, username: currentUserName } = user || {};
// the accessibility tree just needs a little more time to pick up the change.
// This function allows us to set aria-expanded to false and then delay just a bit before setting focus on the button
@@ -106,6 +143,10 @@ function NavLinks({
data-playwright-test-label='header-menu'
className={`nav-list${displayMenu ? ' display-menu' : ''}`}
>
<DonateButton
isUserDonating={isUserDonating}
handleMenuKeyDown={handleMenuKeyDown}
/>
<li key='learn'>
<Link className='nav-link' onKeyDown={handleMenuKeyDown} to='/learn'>
{t('buttons.curriculum')}

View File

@@ -84,9 +84,8 @@
/**
* Site header language list
* Using ~ so it still works if more items are added between the button and list.
*/
.lang-button-nav[aria-expanded='true'] ~ .nav-list {
.lang-button-nav[aria-expanded='true'] + .nav-list {
-ms-overflow-style: none;
display: block;
max-height: calc(100vh - var(--header-height));
@@ -95,7 +94,7 @@
top: calc(var(--header-height));
}
.lang-button-nav[aria-expanded='true'] ~ .nav-list::-webkit-scrollbar {
.lang-button-nav[aria-expanded='true'] + .nav-list::-webkit-scrollbar {
display: none;
}
@@ -259,33 +258,6 @@ li > button.nav-link-signout:not([aria-disabled='true']):is(:hover, :focus) {
border: 1px solid var(--gray-00);
}
.nav-donate-btn .menu-btn-icon {
display: inline-flex;
align-items: center;
}
.nav-donate-btn .menu-btn-text {
display: none;
}
@media (min-width: 601px) {
.nav-donate-btn .menu-btn-icon {
display: none;
}
.nav-donate-btn .menu-btn-text {
display: inline-block;
}
}
.nav-donate-btn .fa-heart {
color: #ff5e5e;
transition: transform 0.2s ease;
}
.nav-donate-btn:hover .fa-heart {
transform: scale(1.2);
}
/**
* User thumbnail
*/
@@ -395,7 +367,7 @@ li > button.nav-link-signout:not([aria-disabled='true']):is(:hover, :focus) {
menu is collapsed. */
.universal-nav-right
#toggle-button-nav[aria-expanded='false']
~ .fcc_searchBar {
+ .fcc_searchBar {
display: none;
}
@@ -403,7 +375,7 @@ li > button.nav-link-signout:not([aria-disabled='true']):is(:hover, :focus) {
menu is collapsed. */
.universal-nav-right
#toggle-button-nav[aria-expanded='false']
~ .fcc_searchBar
+ .fcc_searchBar
.ais-Hits {
display: none;
}
@@ -451,17 +423,12 @@ li > button.nav-link-signout:not([aria-disabled='true']):is(:hover, :focus) {
/**
* Handle submenu containers collapsed and expanded states
* We use ~ because the Donate button is now between the toggle button
* and the menu/search elements, so they are no longer right next to each other.
*/
#universal-nav button[aria-expanded='false'] ~ .nav-list,
#universal-nav button[aria-expanded='false'] ~ .fcc_searchBar {
#universal-nav button[aria-expanded='false'] + div {
display: none;
}
#universal-nav button[aria-expanded='true'] ~ .nav-list,
#universal-nav button[aria-expanded='true'] ~ .fcc_searchBar {
#universal-nav button[aria-expanded='true'] + div {
display: block;
}

View File

@@ -1,103 +0,0 @@
import React from 'react';
import { render, screen, within } from '@testing-library/react';
import { describe, it, expect, vi } from 'vitest';
import { Provider } from 'react-redux';
import { createStore } from '../../../redux/create-store';
import UniversalNav from './universal-nav';
vi.mock('@loadable/component', () => ({
default: () => {
const LazyComponent = () => null;
LazyComponent.displayName = 'Loadable';
return LazyComponent;
}
}));
vi.mock('../../../utils/get-words');
vi.mock('../../../analytics');
const baseProps = {
displayMenu: false,
showMenu: vi.fn(),
hideMenu: vi.fn(),
menuButtonRef: { current: null } as React.RefObject<HTMLButtonElement>,
searchBarRef: { current: null } as React.RefObject<HTMLDivElement>,
fetchState: { pending: false },
pathname: '/learn'
};
const baseUser = {
username: 'test-user',
picture: 'https://freecodecamp.org/image.png',
yearsTopContributor: []
};
const nonDonatingUser = { ...baseUser, isDonating: false };
const donatingUser = { ...baseUser, isDonating: true };
const getByLabel = (label: string) =>
within(screen.getByRole('navigation')).getByRole('link', {
hidden: true,
name: (_: string, el: Element) =>
el.getAttribute('data-playwright-test-label') === label
});
const queryByLabel = (label: string) =>
within(screen.getByRole('navigation')).queryByRole('link', {
hidden: true,
name: (_: string, el: Element) =>
el.getAttribute('data-playwright-test-label') === label
});
const renderNav = (
user: typeof baseUser & { isDonating: boolean },
pending = false
) =>
render(
<Provider store={createStore()}>
<UniversalNav {...baseProps} user={user} fetchState={{ pending }} />
</Provider>
);
describe('<UniversalNav />', () => {
describe.each([
{
label: 'non-donating user',
user: nonDonatingUser,
visibleBtn: { testLabel: 'header-donate-button', href: '/donate' },
hiddenBtn: { testLabel: 'header-support-button' }
},
{
label: 'donating user',
user: donatingUser,
visibleBtn: { testLabel: 'header-support-button', href: '/supporters' },
hiddenBtn: { testLabel: 'header-donate-button' }
}
])('$label', ({ user, visibleBtn, hiddenBtn }) => {
it(`renders ${visibleBtn.testLabel}`, () => {
renderNav(user);
expect(getByLabel(visibleBtn.testLabel)).toBeInTheDocument();
});
it(`links to ${visibleBtn.href}`, () => {
renderNav(user);
expect(getByLabel(visibleBtn.testLabel)).toHaveAttribute(
'href',
visibleBtn.href
);
});
it(`does not render ${hiddenBtn.testLabel}`, () => {
renderNav(user);
expect(queryByLabel(hiddenBtn.testLabel)).not.toBeInTheDocument();
});
});
describe('Loading state', () => {
it('renders no donate or supporters button when pending', () => {
renderNav(nonDonatingUser, true);
expect(queryByLabel('header-donate-button')).not.toBeInTheDocument();
expect(queryByLabel('header-support-button')).not.toBeInTheDocument();
});
});
});

View File

@@ -1,5 +1,3 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faHeart } from '@fortawesome/free-solid-svg-icons';
import Loadable from '@loadable/component';
import React from 'react';
import { useTranslation } from 'react-i18next';
@@ -8,7 +6,6 @@ import { isLanding } from '../../../utils/path-parsers';
import { Link, SkeletonSprite } from '../../helpers';
import { SEARCH_EXPOSED_WIDTH } from '../../../../config/misc';
import FreeCodeCampLogo from '../../../assets/icons/freecodecamp-logo';
import SupporterBadge from '../../../assets/icons/supporter-badge';
import MenuButton from './menu-button';
import NavLinks from './nav-links';
import AuthOrProfile from './auth-or-profile';
@@ -57,8 +54,6 @@ const UniversalNav = ({
) : (
<SearchBar innerRef={searchBarRef} />
);
const isDonating: boolean = user?.isDonating;
return (
<nav
aria-label={t('aria.primary-nav')}
@@ -91,30 +86,6 @@ const UniversalNav = ({
innerRef={menuButtonRef}
showMenu={showMenu}
/>
<Link
className={`nav-donate-btn signup-btn btn-cta ${
isDonating && 'nav-link-supporter'
}`}
sameTab={false}
to={isDonating ? '/supporters' : '/donate'}
data-playwright-test-label={
isDonating ? 'header-support-button' : 'header-donate-button'
}
>
<span className='menu-btn-icon'>
{isDonating ? (
<SupporterBadge />
) : (
<FontAwesomeIcon icon={faHeart} />
)}
<span className='sr-only'>
{isDonating ? t('buttons.supporters') : t('buttons.donate')}
</span>
</span>
<span className='menu-btn-text'>
{isDonating ? t('buttons.supporters') : t('buttons.donate')}
</span>
</Link>
{!isSearchExposedWidth && search}
<NavLinks
displayMenu={displayMenu}

View File

@@ -46,26 +46,6 @@ test.describe('Header', () => {
await expect(skipContent).toHaveAttribute('href', '#content-start');
});
test('Should display universal nav Donate button', async ({ page }) => {
const donateButton = page.getByTestId('header-donate-button');
await expect(donateButton).toBeVisible();
await expect(donateButton).toHaveAttribute('href', '/donate');
});
test('Should show "Donate" text on desktop and Heart icon on mobile in the header', async ({
page,
isMobile
}) => {
const donateButton = page.getByTestId('header-donate-button');
const donateText = donateButton.locator('.menu-btn-text');
if (isMobile) {
await expect(donateText).toBeHidden();
} else {
await expect(donateText).toBeVisible();
}
});
test('Renders universal nav by default', async ({ page }) => {
const universalNavigation = page.getByTestId(
headerComponentElements.universalNav
@@ -149,9 +129,7 @@ test.describe('Header', () => {
await expect(menuButton).toBeVisible();
await menuButton.click();
const link = menu.getByRole('link', {
name: translations.buttons.curriculum
});
const link = menu.getByRole('link', { name: translations.buttons.donate });
await link.focus();
await page.keyboard.press('Escape');
@@ -160,7 +138,7 @@ test.describe('Header', () => {
await expect(menuButton).toBeFocused();
});
test('The menu should contain links to: curriculum, catalog, forum, news, radio, contribute, and podcast', async ({
test('The menu should contain links to: donate, curriculum, catalog, forum, news, radio, contribute, and podcast', async ({
page
}) => {
const menuButton = page.getByTestId(headerComponentElements.menuButton);
@@ -170,6 +148,11 @@ test.describe('Header', () => {
await expect(menu).toBeVisible();
const menuLinks = [
{ name: translations.buttons.profile, href: '/developmentuser' },
{
name: translations.buttons.donate,
href: '/donate'
},
{
name: translations.buttons.curriculum,
href: '/learn'