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:
Divyansh Singh
2022-10-06 00:13:00 +05:30
committed by GitHub
parent edde1b7bf0
commit 6dd8c6241b
18 changed files with 67 additions and 112 deletions
+12
View File
@@ -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;
}
+3
View File
@@ -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}
+1 -1
View File
@@ -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')}`}
+18 -1
View File
@@ -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}
@@ -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}
+11 -2
View File
@@ -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();
}