mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-05-28 18:26:54 +00:00
fix(UI): minimize the fixed contents upper the editor (#54978)
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
export const MAX_MOBILE_WIDTH = 767;
|
||||
export const EX_SMALL_VIEWPORT_HEIGHT = 300;
|
||||
export const TOOL_PANEL_HEIGHT = 37;
|
||||
export const SEARCH_EXPOSED_WIDTH = 980;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { ReactNode, useEffect } from 'react';
|
||||
import Helmet from 'react-helmet';
|
||||
import { useTranslation, withTranslation } from 'react-i18next';
|
||||
import { useMediaQuery } from 'react-responsive';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
@@ -43,7 +44,11 @@ import StagingWarningModal from '../staging-warning-modal';
|
||||
import Footer from '../Footer';
|
||||
import Header from '../Header';
|
||||
import OfflineWarning from '../OfflineWarning';
|
||||
import { Loader } from '../helpers';
|
||||
import { Loader, Spacer } from '../helpers';
|
||||
import {
|
||||
MAX_MOBILE_WIDTH,
|
||||
EX_SMALL_VIEWPORT_HEIGHT
|
||||
} from '../../../config/misc';
|
||||
import envData from '../../../config/env.json';
|
||||
|
||||
import '@freecodecamp/ui/dist/base.css';
|
||||
@@ -103,6 +108,7 @@ interface DefaultLayoutProps extends StateProps, DispatchProps {
|
||||
pathname: string;
|
||||
showFooter?: boolean;
|
||||
isChallenge?: boolean;
|
||||
usesMultifileEditor?: boolean;
|
||||
block?: string;
|
||||
examInProgress: boolean;
|
||||
superBlock?: string;
|
||||
@@ -127,6 +133,7 @@ function DefaultLayout({
|
||||
removeFlashMessage,
|
||||
showFooter = true,
|
||||
isChallenge = false,
|
||||
usesMultifileEditor,
|
||||
block,
|
||||
superBlock,
|
||||
theme,
|
||||
@@ -135,6 +142,14 @@ function DefaultLayout({
|
||||
updateAllChallengesInfo
|
||||
}: DefaultLayoutProps): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const isMobileLayout = useMediaQuery({ maxWidth: MAX_MOBILE_WIDTH });
|
||||
const isProject = /project$/.test(block as string);
|
||||
const isRenderBreadcrumbOnMobile =
|
||||
isMobileLayout && (isProject || !usesMultifileEditor);
|
||||
const isRenderBreadcrumb = !isMobileLayout || isRenderBreadcrumbOnMobile;
|
||||
const isExSmallViewportHeight = useMediaQuery({
|
||||
maxHeight: EX_SMALL_VIEWPORT_HEIGHT
|
||||
});
|
||||
const { challengeEdges, certificateNodes } = useGetAllBlockIds();
|
||||
useEffect(() => {
|
||||
// componentDidMount
|
||||
@@ -243,13 +258,17 @@ function DefaultLayout({
|
||||
/>
|
||||
) : null}
|
||||
<SignoutModal />
|
||||
{isChallenge && !examInProgress && (
|
||||
{isChallenge && !examInProgress && isRenderBreadcrumb ? (
|
||||
<div className='breadcrumbs-demo'>
|
||||
<BreadCrumb
|
||||
block={block as string}
|
||||
superBlock={superBlock as string}
|
||||
/>
|
||||
</div>
|
||||
) : isExSmallViewportHeight ? (
|
||||
<Spacer size='xxSmall' />
|
||||
) : (
|
||||
<Spacer size='small' />
|
||||
)}
|
||||
{fetchState.complete && children}
|
||||
</div>
|
||||
|
||||
@@ -190,12 +190,16 @@
|
||||
|
||||
#mobile-layout .portal-button-wrap {
|
||||
position: absolute;
|
||||
top: calc(var(--header-height) + var(--breadcrumbs-height));
|
||||
top: auto;
|
||||
right: 0;
|
||||
height: 2rem;
|
||||
border-bottom: 1px solid var(--quaternary-color);
|
||||
}
|
||||
|
||||
#mobile-layout .tab-content[data-state='active'] .portal-button-wrap {
|
||||
transform: translateY(-2rem);
|
||||
}
|
||||
|
||||
#mobile-layout #mobile-layout-pane-instructions {
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -224,3 +228,50 @@
|
||||
.nav-lists [role='tab']:focus {
|
||||
outline-offset: -3px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 480px) {
|
||||
#mobile-layout .monaco-editor-tabs {
|
||||
padding: 10px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-height: 300px) {
|
||||
#mobile-layout .nav-lists {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
#mobile-layout .nav-lists > button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#mobile-layout .portal-button-wrap {
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
#mobile-layout .tab-content[data-state='active'] .portal-button-wrap {
|
||||
height: 1.52rem;
|
||||
transform: translateY(-1.5rem);
|
||||
}
|
||||
|
||||
#mobile-layout .portal-button-wrap button {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 1.43rem;
|
||||
font-size: initial;
|
||||
}
|
||||
|
||||
#mobile-layout .portal-button-wrap button svg {
|
||||
scale: 0.8;
|
||||
}
|
||||
|
||||
#mobile-layout .monaco-editor-tabs {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.monaco-editor-tabs button {
|
||||
padding: 2px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
challengeFilesSelector
|
||||
} from '../redux/selectors';
|
||||
|
||||
import { MAX_MOBILE_WIDTH } from '../../../../config/misc';
|
||||
import type { VisibleEditors } from './multifile-editor';
|
||||
|
||||
interface EditorTabsProps {
|
||||
@@ -34,32 +35,41 @@ const mapDispatchToProps = {
|
||||
|
||||
class EditorTabs extends Component<EditorTabsProps> {
|
||||
static displayName: string;
|
||||
isMobile = window.innerWidth < MAX_MOBILE_WIDTH;
|
||||
render() {
|
||||
const { challengeFiles, toggleVisibleEditor, visibleEditors } = this.props;
|
||||
const isMobile = window.innerWidth < MAX_MOBILE_WIDTH;
|
||||
const isRenderChallengeFiles =
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access */
|
||||
!isMobile || sortChallengeFiles(challengeFiles).length > 1;
|
||||
return (
|
||||
<div className='monaco-editor-tabs'>
|
||||
{/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */}
|
||||
{sortChallengeFiles(challengeFiles).map(
|
||||
(challengeFile: ChallengeFile) => (
|
||||
<button
|
||||
aria-expanded={
|
||||
// @ts-expect-error TODO: validate challengeFile on io-boundary,
|
||||
// then we won't need to ignore this error and we can drop the
|
||||
// nullish handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
visibleEditors[challengeFile.fileKey] ?? 'false'
|
||||
}
|
||||
key={challengeFile.fileKey}
|
||||
onClick={() => toggleVisibleEditor(challengeFile.fileKey)}
|
||||
>
|
||||
{`${challengeFile.name}.${challengeFile.ext}`}{' '}
|
||||
<span className='sr-only'>
|
||||
{i18next.t('learn.editor-tabs.editor')}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
isRenderChallengeFiles && (
|
||||
<div className='monaco-editor-tabs'>
|
||||
{
|
||||
/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */
|
||||
sortChallengeFiles(challengeFiles).map(
|
||||
(challengeFile: ChallengeFile) => (
|
||||
<button
|
||||
aria-expanded={
|
||||
// @ts-expect-error TODO: validate challengeFile on io-boundary,
|
||||
// then we won't need to ignore this error and we can drop the
|
||||
// nullish handling.
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
visibleEditors[challengeFile.fileKey] ?? 'false'
|
||||
}
|
||||
key={challengeFile.fileKey}
|
||||
onClick={() => toggleVisibleEditor(challengeFile.fileKey)}
|
||||
>
|
||||
{`${challengeFile.name}.${challengeFile.ext}`}{' '}
|
||||
<span className='sr-only'>
|
||||
{i18next.t('learn.editor-tabs.editor')}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
)
|
||||
}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,13 @@ textarea.inputarea {
|
||||
height: var(--breadcrumbs-height);
|
||||
}
|
||||
|
||||
@media screen and (max-height: 300px) {
|
||||
.default-layout:has(#mobile-layout) .breadcrumbs-demo {
|
||||
height: auto;
|
||||
padding-block: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.editor-upper-jaw,
|
||||
.editor-lower-jaw {
|
||||
padding-inline: 0 15px;
|
||||
@@ -65,6 +72,51 @@ textarea.inputarea {
|
||||
margin: 0.1em 0 0.6rem;
|
||||
}
|
||||
|
||||
.description-container .breadcrumbs {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
gap: 1rem;
|
||||
list-style-type: none;
|
||||
font-size: 16px;
|
||||
text-align: center;
|
||||
margin-bottom: 0.8rem;
|
||||
padding-inline-start: 0;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
color: var(--foreground-quaternary);
|
||||
}
|
||||
|
||||
.description-container .breadcrumbs li:first-child {
|
||||
position: relative;
|
||||
max-width: 35%;
|
||||
}
|
||||
|
||||
.description-container .breadcrumbs li:first-child:after {
|
||||
content: ' >';
|
||||
font-size: 17px;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -0.7rem;
|
||||
}
|
||||
|
||||
.description-container .breadcrumbs li:nth-child(2) {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.description-container .breadcrumbs a {
|
||||
width: 100%;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
border-bottom: 1px solid var(--foreground-quaternary);
|
||||
}
|
||||
|
||||
.description-container .breadcrumbs a:focus {
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
.description-container h1 {
|
||||
color: var(--secondary-color);
|
||||
font-size: 1.1rem;
|
||||
|
||||
@@ -77,6 +77,8 @@ export interface EditorProps {
|
||||
challengeFiles: ChallengeFiles;
|
||||
challengeType: number;
|
||||
containerRef?: React.RefObject<HTMLElement>;
|
||||
block: string;
|
||||
superBlock: string;
|
||||
description: string;
|
||||
dimensions?: Dimensions;
|
||||
editorRef: MutableRefObject<editor.IStandaloneCodeEditor | undefined>;
|
||||
@@ -243,7 +245,7 @@ const initialData: EditorProperties = {
|
||||
const Editor = (props: EditorProps): JSX.Element => {
|
||||
const reduxStore = useStore();
|
||||
const { t } = useTranslation();
|
||||
const { editorRef, initTests, resetAttempts } = props;
|
||||
const { editorRef, initTests, resetAttempts, isMobileLayout } = props;
|
||||
// These refs are used during initialisation of the editor as well as by
|
||||
// callbacks. Since they have to be initialised before editorWillMount and
|
||||
// editorDidMount are called, we cannot use useState. Reason being that will
|
||||
@@ -804,6 +806,7 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
descContainer.classList.add('description-container');
|
||||
domNode.classList.add('editor-upper-jaw');
|
||||
domNode.appendChild(descContainer);
|
||||
if (isMobileLayout) descContainer.appendChild(createBreadcrumb());
|
||||
descContainer.appendChild(jawHeading);
|
||||
descContainer.appendChild(desc);
|
||||
desc.innerHTML = description;
|
||||
@@ -896,6 +899,35 @@ const Editor = (props: EditorProps): JSX.Element => {
|
||||
updateFile({ fileKey, editorValue, editableRegionBoundaries });
|
||||
};
|
||||
|
||||
function createBreadcrumb(): HTMLElement {
|
||||
const { block, superBlock } = props;
|
||||
const breadcrumb = document.createElement('nav');
|
||||
breadcrumb.setAttribute('aria-label', `${t('aria.breadcrumb-nav')}`);
|
||||
const breadcrumbList = document.createElement('ol'),
|
||||
breadcrumbLeft = document.createElement('li'),
|
||||
breadcrumbLeftLink = document.createElement('a'),
|
||||
breadcrumbRight = document.createElement('li'),
|
||||
breadcrumbRightLink = document.createElement('a');
|
||||
breadcrumbLeftLink.innerHTML = t(`intro:${superBlock}.title`);
|
||||
breadcrumbRightLink.innerHTML = t(
|
||||
`intro:${superBlock}.blocks.${block}.title`
|
||||
);
|
||||
breadcrumbLeftLink.setAttribute('href', `/learn/${superBlock}`);
|
||||
breadcrumbRightLink.setAttribute('href', `/learn/${superBlock}/#${block}`);
|
||||
breadcrumbLeft.appendChild(breadcrumbLeftLink);
|
||||
breadcrumbRight.appendChild(breadcrumbRightLink);
|
||||
breadcrumbList.setAttribute(
|
||||
'data-playwright-test-label',
|
||||
'breadcrumb-mobile'
|
||||
);
|
||||
breadcrumbList.className = 'breadcrumbs';
|
||||
breadcrumbList.appendChild(breadcrumbLeft);
|
||||
breadcrumbList.appendChild(breadcrumbRight);
|
||||
breadcrumb.appendChild(breadcrumbList);
|
||||
|
||||
return breadcrumb;
|
||||
}
|
||||
|
||||
// TODO: DRY this and the update function
|
||||
function initializeEditableRegion(
|
||||
range: IRange,
|
||||
|
||||
@@ -35,6 +35,8 @@ type MultifileEditorProps = Pick<
|
||||
| 'initialTests'
|
||||
| 'editorRef'
|
||||
| 'containerRef'
|
||||
| 'block'
|
||||
| 'superBlock'
|
||||
| 'challengeFiles'
|
||||
| 'description'
|
||||
// We use dimensions to trigger a re-render of the editor
|
||||
@@ -65,6 +67,8 @@ const mapStateToProps = createSelector(
|
||||
|
||||
const MultifileEditor = (props: MultifileEditorProps) => {
|
||||
const {
|
||||
block,
|
||||
superBlock,
|
||||
challengeFiles,
|
||||
containerRef,
|
||||
description,
|
||||
@@ -133,6 +137,8 @@ const MultifileEditor = (props: MultifileEditorProps) => {
|
||||
>
|
||||
<Editor
|
||||
canFocusOnMountRef={canFocusOnMountRef}
|
||||
block={block}
|
||||
superBlock={superBlock}
|
||||
challengeFiles={challengeFiles}
|
||||
containerRef={containerRef}
|
||||
description={targetEditor === key ? description : ''}
|
||||
|
||||
@@ -406,6 +406,8 @@ function ShowClassic({
|
||||
reduxChallengeFiles && (
|
||||
<MultifileEditor
|
||||
challengeFiles={reduxChallengeFiles}
|
||||
block={block}
|
||||
superBlock={superBlock}
|
||||
containerRef={containerRef}
|
||||
description={description}
|
||||
editorRef={editorRef}
|
||||
|
||||
@@ -17,7 +17,7 @@ function BreadCrumb({ block, superBlock }: BreadCrumbProps): JSX.Element {
|
||||
className='challenge-title-breadcrumbs'
|
||||
aria-label={t('aria.breadcrumb-nav')}
|
||||
>
|
||||
<ol>
|
||||
<ol data-playwright-test-label='breadcrumb-desktop'>
|
||||
<li className='breadcrumb-left'>
|
||||
<Link
|
||||
state={{ breadcrumbBlockClick: block }}
|
||||
|
||||
@@ -7,6 +7,7 @@ import FourOhFourPage from '../../src/pages/404';
|
||||
interface LayoutSelectorProps {
|
||||
element: JSX.Element;
|
||||
props: {
|
||||
data: { challengeNode?: { challenge?: { usesMultifileEditor?: boolean } } };
|
||||
location: { pathname: string };
|
||||
pageContext?: { challengeMeta?: { block?: string; superBlock?: string } };
|
||||
};
|
||||
@@ -37,6 +38,9 @@ export default function layoutSelector({
|
||||
pathname={pathname}
|
||||
showFooter={false}
|
||||
isChallenge={true}
|
||||
usesMultifileEditor={
|
||||
props.data?.challengeNode?.challenge?.usesMultifileEditor
|
||||
}
|
||||
block={props.pageContext?.challengeMeta?.block}
|
||||
superBlock={props.pageContext?.challengeMeta?.superBlock}
|
||||
>
|
||||
|
||||
+38
-23
@@ -2,34 +2,49 @@ import { test, expect } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto(
|
||||
'/learn/foundational-c-sharp-with-microsoft/write-your-first-code-using-c-sharp/trophy-write-your-first-code-using-c-sharp'
|
||||
'/learn/2022/responsive-web-design/learn-html-by-building-a-cat-photo-app/step-2'
|
||||
);
|
||||
});
|
||||
|
||||
test.describe('Challenge Breadcrumb Component Tests', () => {
|
||||
test('should display correctly', async ({ page }) => {
|
||||
const superBlock = page.getByRole('listitem').first();
|
||||
await expect(superBlock).toBeVisible();
|
||||
test.describe('Challenge Breadcrumb Tests', () => {
|
||||
test('should display correctly', async ({ page, isMobile }) => {
|
||||
const breadcrumbTest = async (testId: string) => {
|
||||
const superBlock = page.getByTestId(testId).getByRole('listitem').first();
|
||||
await expect(superBlock).toBeVisible();
|
||||
|
||||
const superBlockLink = superBlock.getByRole('link', {
|
||||
name: '(New) Foundational C# with Microsoft'
|
||||
});
|
||||
await expect(superBlockLink).toBeVisible();
|
||||
await expect(superBlockLink).toHaveAttribute(
|
||||
'href',
|
||||
'/learn/foundational-c-sharp-with-microsoft'
|
||||
);
|
||||
const superBlockLink = superBlock.getByRole('link', {
|
||||
name: 'Responsive Web Design'
|
||||
});
|
||||
await expect(superBlockLink).toBeVisible();
|
||||
await expect(superBlockLink).toHaveAttribute(
|
||||
'href',
|
||||
'/learn/2022/responsive-web-design'
|
||||
);
|
||||
|
||||
const block = page.getByRole('listitem').last();
|
||||
await expect(superBlock).toBeVisible();
|
||||
const block = page.getByTestId(testId).getByRole('listitem').last();
|
||||
await expect(superBlock).toBeVisible();
|
||||
|
||||
const blockLink = block.getByRole('link', {
|
||||
name: 'Write Your First Code Using C#'
|
||||
});
|
||||
await expect(blockLink).toBeVisible();
|
||||
await expect(blockLink).toHaveAttribute(
|
||||
'href',
|
||||
'/learn/foundational-c-sharp-with-microsoft/#write-your-first-code-using-c-sharp'
|
||||
);
|
||||
const blockLink = block.getByRole('link', {
|
||||
name: 'Learn HTML by Building a Cat Photo App'
|
||||
});
|
||||
await expect(blockLink).toBeVisible();
|
||||
await expect(blockLink).toHaveAttribute(
|
||||
'href',
|
||||
'/learn/2022/responsive-web-design/#learn-html-by-building-a-cat-photo-app'
|
||||
);
|
||||
};
|
||||
|
||||
if (!isMobile) {
|
||||
await expect(page.getByTestId('breadcrumb-mobile')).toBeHidden();
|
||||
await breadcrumbTest('breadcrumb-desktop');
|
||||
|
||||
await page.setViewportSize({
|
||||
width: 766,
|
||||
height: 1080
|
||||
});
|
||||
}
|
||||
|
||||
await expect(page.getByTestId('breadcrumb-desktop')).toBeHidden();
|
||||
await breadcrumbTest('breadcrumb-mobile');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user