mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-04-04 13:00:18 -04:00
revert(client): ensure donate button is always visible (#66754)
This commit is contained in:
committed by
GitHub
parent
72ddb7a992
commit
a72fe07399
@@ -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')}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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}
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user