diff --git a/client/i18n/locales/english/translations.json b/client/i18n/locales/english/translations.json index db761fa6607..7694fb5e639 100644 --- a/client/i18n/locales/english/translations.json +++ b/client/i18n/locales/english/translations.json @@ -510,7 +510,8 @@ "step": "Step", "steps": "Steps", "steps-for": "Steps for {{blockTitle}}", - "code-example": "{{codeName}} code example" + "code-example": "{{codeName}} code example", + "opens-new-window": "Opens in new window" }, "flash": { "honest-first": "To claim a certification, you must first accept our academic honesty policy", diff --git a/client/src/client-only-routes/show-project-links.tsx b/client/src/client-only-routes/show-project-links.tsx index 30df99a8843..94d8a603759 100644 --- a/client/src/client-only-routes/show-project-links.tsx +++ b/client/src/client-only-routes/show-project-links.tsx @@ -83,6 +83,7 @@ const ShowProjectLinks = (props: ShowProjectLinksProps): JSX.Element => { ', () => { it('Render button when only solution is present', () => { // @ts-expect-error render(, store); - const showViewButton = screen.getByRole('link', { name: 'buttons.view' }); + const showViewButton = screen.getByRole('link', { + name: 'buttons.view settings.labels.solution-for (aria.opens-new-window)' + }); expect(showViewButton).toHaveAttribute( 'href', 'https://github.com/freeCodeCamp/freeCodeCamp' @@ -84,7 +86,9 @@ describe('', () => { // @ts-expect-error render(, store); - const viewButtons = screen.getAllByRole('button', { name: 'buttons.view' }); + const viewButtons = screen.getAllByRole('button', { + name: 'buttons.view settings.labels.solution-for' + }); viewButtons.forEach(button => { expect(button).toBeInTheDocument(); }); diff --git a/client/src/components/profile/components/time-line.tsx b/client/src/components/profile/components/time-line.tsx index 9dcdb48934b..b9c19a0d426 100644 --- a/client/src/components/profile/components/time-line.tsx +++ b/client/src/components/profile/components/time-line.tsx @@ -103,12 +103,15 @@ function TimelineInner({ function renderViewButton( completedChallenge: CompletedChallenge ): React.ReactNode { + const { id } = completedChallenge; + const projectTitle = idToNameMap.get(id)?.challengeTitle || ''; return ( viewSolution(completedChallenge)} showProjectPreview={() => viewProject(completedChallenge)} - displayContext={'timeline'} + displayContext='timeline' > ); } diff --git a/client/src/components/settings/certification.js b/client/src/components/settings/certification.js index a44724bdf09..2e81f4cf4be 100644 --- a/client/src/components/settings/certification.js +++ b/client/src/components/settings/certification.js @@ -213,9 +213,10 @@ export class CertificationSettings extends Component { ); }; @@ -285,7 +286,8 @@ export class CertificationSettings extends Component { data-cy={`btn-for-${certSlug}`} onClick={createClickHandler(certSlug)} > - {isCert ? t('buttons.show-cert') : t('buttons.claim-cert')} + {isCert ? t('buttons.show-cert') : t('buttons.claim-cert')}{' '} + {certName} diff --git a/client/src/components/settings/certification.test.tsx b/client/src/components/settings/certification.test.tsx index de7969d34a6..195b64dd920 100644 --- a/client/src/components/settings/certification.test.tsx +++ b/client/src/components/settings/certification.test.tsx @@ -20,7 +20,7 @@ describe('', () => { expect( screen.getByRole('link', { - name: 'buttons.show-cert' + name: /^buttons.show-cert\s+\S+/ }) ).toHaveAttribute( 'href', @@ -33,7 +33,7 @@ describe('', () => { renderWithRedux(); const allClaimedCerts = screen.getAllByRole('link', { - name: 'buttons.show-cert' + name: /^buttons.show-cert\s+\S+/ }); allClaimedCerts.forEach(cert => { @@ -49,7 +49,7 @@ describe('', () => { renderWithRedux(); const allClaimedCerts = screen.getAllByRole('link', { - name: 'buttons.show-cert' + name: /^buttons.show-cert\s+\S+/ }); allClaimedCerts.forEach(cert => { @@ -65,7 +65,7 @@ describe('', () => { expect( screen.getByRole('link', { - name: 'buttons.view' + name: 'buttons.view settings.labels.solution-for (aria.opens-new-window)' }) ).toHaveAttribute('href', 'https://github.com/freeCodeCamp/freeCodeCamp'); }); diff --git a/client/src/components/solution-display-widget/index.tsx b/client/src/components/solution-display-widget/index.tsx index 4ba1a5aa581..50694807034 100644 --- a/client/src/components/solution-display-widget/index.tsx +++ b/client/src/components/solution-display-widget/index.tsx @@ -1,17 +1,15 @@ import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { - Button, - DropdownButton, - MenuItem -} from '@freecodecamp/react-bootstrap'; +import { Button, Dropdown, MenuItem } from '@freecodecamp/react-bootstrap'; import React from 'react'; import { useTranslation } from 'react-i18next'; import { CompletedChallenge } from '../../redux/prop-types'; import { getSolutionDisplayType } from '../../utils/solution-display-type'; +import './solution-display-widget.css'; interface Props { completedChallenge: CompletedChallenge; dataCy?: string; + projectTitle: string; showUserCode: () => void; showProjectPreview?: () => void; displayContext: 'timeline' | 'settings' | 'certification'; @@ -20,6 +18,7 @@ interface Props { export function SolutionDisplayWidget({ completedChallenge, dataCy, + projectTitle, showUserCode, showProjectPreview, displayContext @@ -29,37 +28,48 @@ export function SolutionDisplayWidget({ const viewText = t('buttons.view'); const viewCode = t('buttons.view-code'); const viewProject = t('buttons.view-project'); - + // We need to add a random number for dropdown button id's since there may be + // two dropdowns for the same project on the page. + const randomIdSuffix = Math.floor(Math.random() * 1_000_000); const ShowFilesSolutionForCertification = ( ); const ShowProjectAndGithubLinkForCertification = ( - - - {t('certification.project.solution')} - - - {t('certification.project.source')} - - + + + {viewText}{' '} + + {t('settings.labels.solution-for', { projectTitle })} + + + + + {t('certification.project.solution')} + ({t('aria.opens-new-window')}) + + + + {t('certification.project.source')} + ({t('aria.opens-new-window')}) + + + + ); const ShowProjectLinkForCertification = ( ); const MissingSolutionComponentForCertification = ( @@ -81,57 +96,67 @@ export function SolutionDisplayWidget({ bsStyle='primary' className='btn-invert' data-cy={dataCy} - id={`btn-for-${id}`} onClick={showUserCode} > - {viewText} + {viewText}{' '} + + {t('settings.labels.solution-for', { projectTitle })} + ); const ShowMultifileProjectSolution = (
- - - {viewCode} - - - {viewProject} - - + + + {viewText}{' '} + + {t('settings.labels.solution-for', { projectTitle })} + + + + + {viewCode} + + + {viewProject} + + +
); const ShowProjectAndGithubLinks = (
- - - {t('buttons.frontend')} - - - {t('buttons.backend')} - - + + + {viewText}{' '} + + {t('settings.labels.solution-for', { projectTitle })} + + + + + {t('buttons.frontend')}{' '} + ({t('aria.opens-new-window')}) + + + + {t('buttons.backend')}{' '} + ({t('aria.opens-new-window')}) + + + +
); const ShowProjectLink = ( @@ -140,11 +165,15 @@ export function SolutionDisplayWidget({ bsStyle='primary' className='btn-invert' href={solution} - id={`btn-for-${id}`} rel='noopener noreferrer' target='_blank' > - {viewText} + {viewText}{' '} + + {t('settings.labels.solution-for', { projectTitle })} ( + {t('aria.opens-new-window')}) + + ); const MissingSolutionComponent = diff --git a/client/src/components/solution-display-widget/solution-display-widget.css b/client/src/components/solution-display-widget/solution-display-widget.css new file mode 100644 index 00000000000..4bff674e523 --- /dev/null +++ b/client/src/components/solution-display-widget/solution-display-widget.css @@ -0,0 +1,3 @@ +.solutions-dropdown a[role='menuitem'] { + text-decoration: none; +} diff --git a/client/src/templates/Introduction/components/cert-challenge.tsx b/client/src/templates/Introduction/components/cert-challenge.tsx index 58f57e949ef..f193e852d3e 100644 --- a/client/src/templates/Introduction/components/cert-challenge.tsx +++ b/client/src/templates/Introduction/components/cert-challenge.tsx @@ -130,7 +130,8 @@ const CertChallenge = ({ > {isCertified && userLoaded ? t('buttons.show-cert') - : t('buttons.go-to-settings')} + : t('buttons.go-to-settings')}{' '} + {title} )}