fix(client): use Gatsby Link for internal links (#55350)

Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
Huyen Nguyen
2024-07-10 00:11:55 -07:00
committed by GitHub
parent 4a506f6e06
commit a36ef8daec
9 changed files with 134 additions and 40 deletions
+8 -3
View File
@@ -10,7 +10,7 @@ import {
} from '../../../../shared/config/superblocks';
import { SuperBlockIcon } from '../../assets/icons/superblock-icon';
import LinkButton from '../../assets/icons/link-button';
import { Link, Spacer } from '../helpers';
import { Spacer, ButtonLink } from '../helpers';
import { getSuperBlockTitleForMap } from '../../utils/superblock-map-titles';
import { showUpcomingChanges } from '../../../config/env.json';
@@ -102,13 +102,18 @@ function MapLi({
</div>
</div>
<Link className='btn link-btn btn-lg' to={`/learn/${superBlock}/`}>
<ButtonLink
block
size='large'
className='map-superblock-link'
href={`/learn/${superBlock}/`}
>
<div style={linkSpacingStyle}>
<SuperBlockIcon className='map-icon' superBlock={superBlock} />
{getSuperBlockTitleForMap(superBlock)}
</div>
{landing && <LinkButton />}
</Link>
</ButtonLink>
</li>
</>
);
@@ -0,0 +1,79 @@
import React from 'react';
import { Link as GatsbyLink } from 'gatsby';
import { Button } from '@freecodecamp/ui';
export type ButtonSize = 'small' | 'medium' | 'large';
interface Props extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
children: React.ReactNode;
href: string;
className?: string;
size?: ButtonSize;
onClick?: React.MouseEventHandler<HTMLAnchorElement>;
block?: boolean;
download?: string;
target?: React.HTMLAttributeAnchorTarget;
disabled?: boolean;
}
/**
* ButtonLink is a link that looks like a button.
* The component renders:
* - Internal link with Gatsby's Link component
* - External link with freecodecamp/ui's Button component,
* which renders a plain `<a>` if it receives an `href`.
* NOTE: We probably want to move this component to @freecodecamp/ui.
*/
export const ButtonLink = ({
className,
children,
href,
target,
download,
onClick,
size = 'medium',
block,
disabled
}: Props) => {
// We only need to compute the styles of `size` and `block` for Gatsby Link.
// freecodecamp/ui's Button already has the logic implemented.
const cls = ['btn'];
if (block) cls.push('btn-block');
// The 'btn' class contains the base button styles as well as the `medium` variant styles.
// So we only need to handle `large` and `small`.
if (size === 'large') cls.push('btn-lg');
if (size === 'small') cls.push('btn-sm');
if (className) cls.push(className);
const gatsbyLinkCls = cls.join(' ');
// Links cannot be disabled. So if `disabled` is true,
// we pass the prop to `Button` in order to render a `<button>` instead of an `<a>`.
if (disabled || target === '_blank' || download) {
return (
<Button
variant='primary'
className={className}
href={href}
target={target}
onClick={onClick}
download={download}
size={size}
block={block}
disabled={disabled}
>
{children}
</Button>
);
}
return (
<GatsbyLink
className={gatsbyLinkCls}
to={href}
target={target}
onClick={onClick}
>
{children}
</GatsbyLink>
);
};
+1
View File
@@ -5,3 +5,4 @@ export { default as Spacer } from './spacer';
export { default as Link } from './link';
export { default as LazyImage } from './lazy-image';
export { default as AvatarRenderer } from './avatar-renderer';
export { ButtonLink } from './button-link';
+27 -19
View File
@@ -291,6 +291,7 @@ input[type='submit'],
border-radius: 0;
text-decoration: none;
white-space: pre-line;
text-align: center;
}
button:hover,
@@ -370,13 +371,6 @@ fieldset[disabled] .btn {
width: 100%;
}
/* Equivalent to the `large` size of `@freecodecamp/ui` Button component */
.btn-lg {
padding: 10px 16px;
font-size: 18px;
line-height: 1.3333333;
}
/* Equivalent to the `medium` size of `@freecodecamp/ui` Button component */
.btn {
padding: 6px 12px;
@@ -384,7 +378,21 @@ fieldset[disabled] .btn {
line-height: 1.42857143;
}
.link-btn {
/* Equivalent to the `large` size of `@freecodecamp/ui` Button component */
.btn-lg {
padding: 0.625rem 1rem;
font-size: 24px;
line-height: 1.3333333;
}
/* Equivalent to the `small` size of `@freecodecamp/ui` Button component */
.btn-sm {
padding: 0.25rem 0.625rem;
font-size: 16px;
line-height: 1.5;
}
.map-superblock-link {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
@@ -394,42 +402,42 @@ fieldset[disabled] .btn {
text-align: left;
}
.link-btn.btn-lg svg {
.map-superblock-link.btn-lg svg {
height: 100%;
min-height: 20px;
min-width: 16px;
margin-inline-start: 5px;
}
.link-btn:active {
.map-superblock-link:active {
background-color: var(--quaternary-background);
}
.link-btn:focus,
.link-btn.btn-lg:focus svg {
.map-superblock-link:focus,
.map-superblock-link.btn-lg:focus svg {
fill: var(--quaternary-background);
background-color: var(--secondary-color);
border-color: var(--gray-45);
color: var(--secondary-background);
}
.link-btn:focus:not(:focus-visible),
.link-btn.btn-lg:focus:not(:focus-visible) svg {
.map-superblock-link:focus:not(:focus-visible),
.map-superblock-link.btn-lg:focus:not(:focus-visible) svg {
background-color: var(--quaternary-background);
border-color: var(--secondary-color);
color: var(--secondary-color);
fill: var(--primary-color);
}
.link-btn:hover,
.link-btn.btn-lg:hover svg {
.map-superblock-link:hover,
.map-superblock-link.btn-lg:hover svg {
fill: var(--quaternary-background) !important;
background-color: var(--secondary-color) !important;
border-color: var(--secondary-color) !important;
color: var(--secondary-background) !important;
}
.link-btn.btn-lg svg,
.map-superblock-link.btn-lg svg,
.map-icon,
.cert-header-icon {
flex-shrink: 0;
@@ -440,7 +448,7 @@ fieldset[disabled] .btn {
fill: var(--quaternary-background);
}
.link-btn.btn-lg:hover .map-icon .inverted-color,
.map-superblock-link.btn-lg:hover .map-icon .inverted-color,
.map-arrow-icon {
fill: var(--secondary-color);
}
@@ -463,7 +471,7 @@ fieldset[disabled] .btn {
}
@media (min-width: 700px) {
.link-btn {
.map-superblock-link {
font-size: 1.1rem;
}
.map-icon {
+1 -1
View File
@@ -23,7 +23,7 @@ Intro project buttons and headings
intro to courses section
*/
[dir='rtl'] .link-btn.btn-lg > svg {
[dir='rtl'] .map-superblock-link.btn-lg > svg {
transform: rotate(180deg);
}
@@ -2,11 +2,10 @@ import React from 'react';
import { useTranslation } from 'react-i18next';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { Button } from '@freecodecamp/ui';
import { certificatesByNameSelector } from '../../../redux/selectors';
import type { CurrentCert } from '../../../redux/prop-types';
import { FullWidthRow, Spacer } from '../../helpers';
import { FullWidthRow, Spacer, ButtonLink } from '../../helpers';
const mapStateToProps = (
state: Record<string, unknown>,
@@ -47,16 +46,15 @@ function CertButton({ username, cert }: CertButtonProps): JSX.Element {
const { t } = useTranslation();
return (
<>
<Button
<ButtonLink
block
size='large'
href={`/certification/${username}/${cert.certSlug}`}
data-playwright-test-label='claimed-certification'
>
{t('buttons.view-cert-title', {
certTitle: t(`certification.title.${cert.certSlug}`)
})}
</Button>
</ButtonLink>
<Spacer size='small' />
</>
);
@@ -2,13 +2,12 @@ import { Link } from 'gatsby';
import React from 'react';
import { withTranslation, useTranslation } from 'react-i18next';
import { Button } from '@freecodecamp/ui';
import GreenNotCompleted from '../../../assets/icons/green-not-completed';
import GreenPass from '../../../assets/icons/green-pass';
import { ChallengeWithCompletedNode } from '../../../redux/prop-types';
import { SuperBlocks } from '../../../../../shared/config/superblocks';
import { challengeTypes } from '../../../../../shared/config/challenge-types';
import { ButtonLink } from '../../../components/helpers/button-link';
const getStepNumber = (dashedName: string) => {
// dashedName should be in the format 'step-1' or 'task-1'
@@ -68,12 +67,12 @@ function Challenges({
<>
{firstIncompleteChallenge && (
<div className='challenge-jump-link'>
<Button size='small' href={firstIncompleteChallenge.fields.slug}>
<ButtonLink size='small' href={firstIncompleteChallenge.fields.slug}>
{!isChallengeStarted
? t('buttons.start-project')
: t('buttons.resume-project')}{' '}
{blockTitle && <span className='sr-only'>{blockTitle}</span>}
</Button>
</ButtonLink>
</div>
)}
<nav
+6 -6
View File
@@ -3,8 +3,8 @@ import React from 'react';
import Helmet from 'react-helmet';
import { useTranslation } from 'react-i18next';
import { Container, Button } from '@freecodecamp/ui';
import Spacer from '../../components/helpers/spacer';
import { Container } from '@freecodecamp/ui';
import { Spacer, ButtonLink } from '../../components/helpers';
import FullWidthRow from '../../components/helpers/full-width-row';
import LearnLayout from '../../components/layouts/learn';
import type { MarkdownRemark, AllChallengeNode } from '../../redux/prop-types';
@@ -44,13 +44,13 @@ function IntroductionPage({
/>
</FullWidthRow>
<FullWidthRow>
<Button block size='large' href={firstLessonPath}>
<ButtonLink block size='large' href={firstLessonPath}>
{t('buttons.first-lesson')}
</Button>
</ButtonLink>
<Spacer size='small' />
<Button block size='large' href='/learn'>
<ButtonLink block size='large' href='/learn'>
{t('buttons.view-curriculum')}
</Button>
</ButtonLink>
<Spacer size='small' />
<hr />
</FullWidthRow>
+6 -2
View File
@@ -15,7 +15,9 @@ test.describe('Public profile certifications', () => {
.click();
}
await expect(page.getByTestId('claimed-certification')).toHaveCount(19);
await expect(
page.getByRole('link', { name: /View.+Certification/ })
).toHaveCount(19);
});
test('Should show claimed certifications if the username includes uppercase characters', async ({
@@ -37,7 +39,9 @@ test.describe('Public profile certifications', () => {
}
await page.waitForURL('/certifiedboozer');
await expect(page.getByTestId('claimed-certification')).toHaveCount(19);
await expect(
page.getByRole('link', { name: /View.+Certification/ })
).toHaveCount(19);
});
test.afterAll(() => {