mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
feat: add hidden skip-to-content button (#47577)
* feat: add skip-to-content button * fix: remove content start ids from everywhere and add to default layout * chore: format landing-top.tsx * use single quotes in skip-to-content component * include breadcrumbs in navigation * linting fail fix * use anchor tag instead of new component * update challenge title snap * fix(test): reliably move focus onto the editor Co-authored-by: Oliver Eyton-Williams <ojeytonwilliams@gmail.com>
This commit is contained in:
@@ -4,3 +4,15 @@ header {
|
||||
width: 100%;
|
||||
z-index: 200;
|
||||
}
|
||||
.skip-to-content-button {
|
||||
position: absolute;
|
||||
top: var(--header-height);
|
||||
border-radius: 15px;
|
||||
font-weight: 600;
|
||||
padding-block: 1em;
|
||||
padding-inline: 1.5em;
|
||||
left: -1000px;
|
||||
}
|
||||
.skip-to-content-button:focus {
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
@@ -91,6 +91,9 @@ export class Header extends React.Component<
|
||||
</style>
|
||||
</Helmet>
|
||||
<header>
|
||||
<a href='#content-start' className='skip-to-content-button'>
|
||||
Skip To Content
|
||||
</a>
|
||||
<UniversalNav
|
||||
displayMenu={displayMenu}
|
||||
fetchState={fetchState}
|
||||
|
||||
@@ -39,7 +39,7 @@ const Intro = ({
|
||||
return (
|
||||
<>
|
||||
<Spacer />
|
||||
<h1 className='text-center '>
|
||||
<h1 className='text-center'>
|
||||
{name
|
||||
? `${t('learn.welcome-1', { name: name })}`
|
||||
: `${t('learn.welcome-2')}`}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
userFetchStateSelector
|
||||
} from '../../redux/selectors';
|
||||
import { UserFetchState, User } from '../../redux/prop-types';
|
||||
import BreadCrumb from '../../templates/Challenges/components/bread-crumb';
|
||||
import Flash from '../Flash';
|
||||
import { flashMessageSelector, removeFlashMessage } from '../Flash/redux';
|
||||
|
||||
@@ -84,6 +85,9 @@ interface DefaultLayoutProps extends StateProps, DispatchProps {
|
||||
children: ReactNode;
|
||||
pathname: string;
|
||||
showFooter?: boolean;
|
||||
isChallenge?: boolean;
|
||||
block?: string;
|
||||
superBlock?: string;
|
||||
t: TFunction;
|
||||
useTheme?: boolean;
|
||||
}
|
||||
@@ -133,6 +137,9 @@ class DefaultLayout extends Component<DefaultLayoutProps> {
|
||||
isSignedIn,
|
||||
removeFlashMessage,
|
||||
showFooter = true,
|
||||
isChallenge = false,
|
||||
block,
|
||||
superBlock,
|
||||
t,
|
||||
theme = 'default',
|
||||
user,
|
||||
@@ -211,7 +218,17 @@ class DefaultLayout extends Component<DefaultLayoutProps> {
|
||||
removeFlashMessage={removeFlashMessage}
|
||||
/>
|
||||
) : null}
|
||||
{fetchState.complete && children}
|
||||
{isChallenge && (
|
||||
<div className='breadcrumbs-demo'>
|
||||
<BreadCrumb
|
||||
block={block as string}
|
||||
superBlock={superBlock as string}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div id='content-start' tabIndex={-1}>
|
||||
{fetchState.complete && children}
|
||||
</div>
|
||||
</div>
|
||||
{showFooter && <Footer />}
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,9 @@ import React from 'react';
|
||||
import { faExternalLinkAlt } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import BreadCrumb from '../components/bread-crumb';
|
||||
import EditorTabs from './editor-tabs';
|
||||
|
||||
interface ActionRowProps {
|
||||
block: string;
|
||||
hasNotes: boolean;
|
||||
isProjectBasedChallenge: boolean;
|
||||
showConsole: boolean;
|
||||
@@ -15,7 +12,6 @@ interface ActionRowProps {
|
||||
showInstructions: boolean;
|
||||
showPreviewPane: boolean;
|
||||
showPreviewPortal: boolean;
|
||||
superBlock: string;
|
||||
togglePane: (pane: string) => void;
|
||||
}
|
||||
|
||||
@@ -27,9 +23,7 @@ const ActionRow = ({
|
||||
showPreviewPortal,
|
||||
showConsole,
|
||||
showInstructions,
|
||||
isProjectBasedChallenge,
|
||||
superBlock,
|
||||
block
|
||||
isProjectBasedChallenge
|
||||
}: ActionRowProps): JSX.Element => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
@@ -57,9 +51,6 @@ const ActionRow = ({
|
||||
|
||||
return (
|
||||
<div className='action-row'>
|
||||
<div className='breadcrumbs-demo'>
|
||||
<BreadCrumb block={block} superBlock={superBlock} />
|
||||
</div>
|
||||
<div className='tabs-row'>
|
||||
{!isProjectBasedChallenge && (
|
||||
<button
|
||||
|
||||
@@ -14,7 +14,6 @@ import ActionRow from './action-row';
|
||||
type Pane = { flex: number };
|
||||
|
||||
interface DesktopLayoutProps {
|
||||
block: string;
|
||||
challengeFiles: ChallengeFiles;
|
||||
challengeType: number;
|
||||
editor: ReactElement | null;
|
||||
@@ -33,7 +32,6 @@ interface DesktopLayoutProps {
|
||||
notes: ReactElement;
|
||||
preview: ReactElement;
|
||||
resizeProps: ResizeProps;
|
||||
superBlock: string;
|
||||
testOutput: ReactElement;
|
||||
windowTitle: string;
|
||||
}
|
||||
@@ -83,7 +81,6 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
};
|
||||
|
||||
const {
|
||||
block,
|
||||
challengeType,
|
||||
resizeProps,
|
||||
instructions,
|
||||
@@ -95,7 +92,6 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
notes,
|
||||
preview,
|
||||
hasEditableBoundaries,
|
||||
superBlock,
|
||||
windowTitle
|
||||
} = props;
|
||||
|
||||
@@ -121,7 +117,6 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
<div className='desktop-layout'>
|
||||
{(projectBasedChallenge || isMultifileCertProject) && (
|
||||
<ActionRow
|
||||
block={block}
|
||||
hasNotes={hasNotes}
|
||||
isProjectBasedChallenge={projectBasedChallenge}
|
||||
showConsole={showConsole}
|
||||
@@ -129,7 +124,6 @@ const DesktopLayout = (props: DesktopLayoutProps): JSX.Element => {
|
||||
showInstructions={showInstructions}
|
||||
showPreviewPane={showPreviewPane}
|
||||
showPreviewPortal={showPreviewPortal}
|
||||
superBlock={superBlock}
|
||||
togglePane={togglePane}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -28,7 +28,8 @@ textarea.inputarea {
|
||||
|
||||
.breadcrumbs-demo {
|
||||
font-size: 16px;
|
||||
margin: 0 0 1.2rem;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.editor-upper-jaw,
|
||||
|
||||
@@ -365,17 +365,13 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
renderInstructionsPanel({ showToolPanel }: { showToolPanel: boolean }) {
|
||||
const {
|
||||
block,
|
||||
challengeType,
|
||||
description,
|
||||
forumTopicId,
|
||||
instructions,
|
||||
superBlock,
|
||||
title,
|
||||
translationPending
|
||||
} = this.getChallenge();
|
||||
|
||||
const showBreadCrumbs =
|
||||
challengeType !== challengeTypes.multifileCertProject;
|
||||
return (
|
||||
<SidePanel
|
||||
block={block}
|
||||
@@ -388,10 +384,7 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
}
|
||||
challengeTitle={
|
||||
<ChallengeTitle
|
||||
block={block}
|
||||
isCompleted={this.props.isChallengeCompleted}
|
||||
showBreadCrumbs={showBreadCrumbs}
|
||||
superBlock={superBlock}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
@@ -534,7 +527,6 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
</Media>
|
||||
<Media minWidth={MAX_MOBILE_WIDTH + 1}>
|
||||
<DesktopLayout
|
||||
block={block}
|
||||
challengeFiles={challengeFiles}
|
||||
challengeType={challengeType}
|
||||
editor={this.renderEditor({
|
||||
@@ -551,7 +543,6 @@ class ShowClassic extends Component<ShowClassicProps, ShowClassicState> {
|
||||
notes={this.renderNotes(notes)}
|
||||
preview={this.renderPreview()}
|
||||
resizeProps={this.resizeProps}
|
||||
superBlock={superBlock}
|
||||
testOutput={this.renderTestOutput()}
|
||||
windowTitle={windowTitle}
|
||||
/>
|
||||
|
||||
@@ -256,9 +256,7 @@ class ShowCodeAlly extends Component<ShowCodeAllyProps> {
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<ChallengeTitle
|
||||
block={block}
|
||||
isCompleted={isChallengeCompleted}
|
||||
superBlock={superBlock}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
|
||||
-35
@@ -4,41 +4,6 @@ exports[`<ChallengeTitle/> renders correctly 1`] = `
|
||||
<div
|
||||
className="challenge-title-wrap"
|
||||
>
|
||||
<nav
|
||||
aria-label="aria.breadcrumb-nav"
|
||||
className="challenge-title-breadcrumbs"
|
||||
>
|
||||
<ol>
|
||||
<li
|
||||
className="breadcrumb-left"
|
||||
>
|
||||
<a
|
||||
href="/learn/fake-superblock"
|
||||
state={
|
||||
Object {
|
||||
"breadcrumbBlockClick": "fake-block",
|
||||
}
|
||||
}
|
||||
>
|
||||
<span
|
||||
className="ellipsis"
|
||||
/>
|
||||
</a>
|
||||
</li>
|
||||
<li
|
||||
className="breadcrumb-right"
|
||||
>
|
||||
<a
|
||||
href="/learn/fake-superblock/#fake-block"
|
||||
state={
|
||||
Object {
|
||||
"breadcrumbBlockClick": "fake-block",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
<div
|
||||
className="challenge-title"
|
||||
>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
.challenge-title-wrap {
|
||||
text-align: center;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.challenge-title {
|
||||
|
||||
@@ -4,10 +4,8 @@ import renderer from 'react-test-renderer';
|
||||
import ChallengeTitle from './challenge-title';
|
||||
|
||||
const baseProps = {
|
||||
block: 'fake-block',
|
||||
children: 'title text',
|
||||
isCompleted: true,
|
||||
superBlock: 'fake-superblock',
|
||||
translationPending: false
|
||||
};
|
||||
|
||||
|
||||
@@ -2,25 +2,18 @@ import i18next from 'i18next';
|
||||
import React from 'react';
|
||||
import GreenPass from '../../../assets/icons/green-pass';
|
||||
import { Link } from '../../../components/helpers/index';
|
||||
import BreadCrumb from './bread-crumb';
|
||||
|
||||
import './challenge-title.css';
|
||||
|
||||
interface ChallengeTitleProps {
|
||||
block: string;
|
||||
children: string;
|
||||
isCompleted: boolean;
|
||||
showBreadCrumbs?: boolean;
|
||||
superBlock: string;
|
||||
translationPending: boolean;
|
||||
}
|
||||
|
||||
function ChallengeTitle({
|
||||
block,
|
||||
children,
|
||||
isCompleted,
|
||||
showBreadCrumbs = true,
|
||||
superBlock,
|
||||
translationPending
|
||||
}: ChallengeTitleProps): JSX.Element {
|
||||
return (
|
||||
@@ -36,7 +29,6 @@ function ChallengeTitle({
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
{showBreadCrumbs && <BreadCrumb block={block} superBlock={superBlock} />}
|
||||
<div className='challenge-title'>
|
||||
<div className='title-text'>
|
||||
<h1>{children}</h1>
|
||||
|
||||
@@ -233,9 +233,7 @@ class BackEnd extends Component<BackEndProps> {
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<ChallengeTitle
|
||||
block={block}
|
||||
isCompleted={isChallengeCompleted}
|
||||
superBlock={superBlock}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -174,9 +174,7 @@ class Project extends Component<ProjectProps> {
|
||||
<Col md={8} mdOffset={2} sm={10} smOffset={1} xs={12}>
|
||||
<Spacer />
|
||||
<ChallengeTitle
|
||||
block={block}
|
||||
isCompleted={isChallengeCompleted}
|
||||
superBlock={superBlock}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -218,9 +218,7 @@ class ShowVideo extends Component<ShowVideoProps, ShowVideoState> {
|
||||
<Row>
|
||||
<Spacer />
|
||||
<ChallengeTitle
|
||||
block={block}
|
||||
isCompleted={isChallengeCompleted}
|
||||
superBlock={superBlock}
|
||||
translationPending={translationPending}
|
||||
>
|
||||
{title}
|
||||
|
||||
@@ -9,7 +9,10 @@ import { isChallenge } from '../../src/utils/path-parsers';
|
||||
|
||||
interface LayoutSelectorProps {
|
||||
element: JSX.Element;
|
||||
props: { location: { pathname: string } };
|
||||
props: {
|
||||
location: { pathname: string };
|
||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
|
||||
};
|
||||
}
|
||||
export default function layoutSelector({
|
||||
element,
|
||||
@@ -31,7 +34,13 @@ export default function layoutSelector({
|
||||
);
|
||||
} else if (isChallenge(pathname)) {
|
||||
return (
|
||||
<DefaultLayout pathname={pathname} showFooter={false}>
|
||||
<DefaultLayout
|
||||
pathname={pathname}
|
||||
showFooter={false}
|
||||
isChallenge={true}
|
||||
block={props.pageContext?.challengeMeta?.block}
|
||||
superBlock={props.pageContext?.challengeMeta?.superBlock}
|
||||
>
|
||||
{element}
|
||||
</DefaultLayout>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const selectors = {
|
||||
defaultOutput: '.output-text',
|
||||
editor: 'div.monaco-editor textarea',
|
||||
editor: 'div.monaco-editor',
|
||||
hotkeys: '.default-layout > div',
|
||||
runTestsButton: 'button:contains("Run the Tests")'
|
||||
};
|
||||
@@ -51,11 +51,7 @@ describe('Classic challenge', function () {
|
||||
});
|
||||
|
||||
it('shows test output when the tests are triggered by the keyboard', () => {
|
||||
// first wait for the editor to load
|
||||
cy.get(selectors.editor, {
|
||||
timeout: 15000
|
||||
})
|
||||
.focus()
|
||||
focusEditor()
|
||||
.type('{ctrl}{enter}')
|
||||
.then(() => {
|
||||
cy.get(selectors.defaultOutput)
|
||||
@@ -90,38 +86,33 @@ describe('jQuery challenge', function () {
|
||||
describe('Custom output for JavaScript objects', function () {
|
||||
beforeEach(() => {
|
||||
cy.visit(locations.js);
|
||||
cy.get(selectors.editor, {
|
||||
timeout: 15000
|
||||
})
|
||||
.first()
|
||||
.click()
|
||||
.focused()
|
||||
.type('{ctrl}a')
|
||||
.clear();
|
||||
focusEditor().type('{ctrl}a').clear();
|
||||
});
|
||||
|
||||
it('Set object', () => {
|
||||
cy.get(selectors.editor)
|
||||
.first()
|
||||
.click()
|
||||
.focused()
|
||||
.type(
|
||||
'const set = new Set();{enter}set.add(1);{enter}set.add("set");{enter}set.add(10);{enter}console.log(set);'
|
||||
);
|
||||
focusEditor().type(
|
||||
'const set = new Set();{enter}set.add(1);{enter}set.add("set");{enter}set.add(10);{enter}console.log(set);'
|
||||
);
|
||||
cy.get(selectors.defaultOutput).should('contain', 'Set(3) {1, set, 10}');
|
||||
});
|
||||
|
||||
it('Map object', () => {
|
||||
cy.get(selectors.editor)
|
||||
.first()
|
||||
.click()
|
||||
.focused()
|
||||
.type(
|
||||
'const map = new Map();{enter}map.set("first", 1);{enter}map.set("second", 2);{enter}map.set("other", "map");{enter}console.log(map);'
|
||||
);
|
||||
focusEditor().type(
|
||||
'const map = new Map();{enter}map.set("first", 1);{enter}map.set("second", 2);{enter}map.set("other", "map");{enter}console.log(map);'
|
||||
);
|
||||
cy.get(selectors.defaultOutput).should(
|
||||
'contain',
|
||||
'Map(3) {first => 1, second => 2, other => map})'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
function focusEditor() {
|
||||
return cy
|
||||
.get(selectors.editor, {
|
||||
timeout: 15000 // first wait for the editor to load
|
||||
})
|
||||
.first()
|
||||
.click()
|
||||
.focused();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user