From 2847c7c77a2a65826e3aa30dfde32001568cda06 Mon Sep 17 00:00:00 2001 From: Manabu Matsumoto Date: Sat, 22 Jun 2024 00:47:09 +0900 Subject: [PATCH] fix(UI): minimize the fixed contents upper the editor (#54978) --- client/config/misc.ts | 1 + client/src/components/layouts/default.tsx | 23 ++++++- .../templates/Challenges/classic/classic.css | 53 +++++++++++++++- .../Challenges/classic/editor-tabs.tsx | 56 ++++++++++------- .../templates/Challenges/classic/editor.css | 52 ++++++++++++++++ .../templates/Challenges/classic/editor.tsx | 34 ++++++++++- .../Challenges/classic/multifile-editor.tsx | 6 ++ .../src/templates/Challenges/classic/show.tsx | 2 + .../Challenges/components/bread-crumb.tsx | 2 +- client/utils/gatsby/layout-selector.tsx | 4 ++ e2e/bread-crumb.spec.ts | 61 ++++++++++++------- 11 files changed, 243 insertions(+), 51 deletions(-) diff --git a/client/config/misc.ts b/client/config/misc.ts index dffa8cb01be..d2fb61fa9a1 100644 --- a/client/config/misc.ts +++ b/client/config/misc.ts @@ -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; diff --git a/client/src/components/layouts/default.tsx b/client/src/components/layouts/default.tsx index e6304b4cccd..646944ecfcc 100644 --- a/client/src/components/layouts/default.tsx +++ b/client/src/components/layouts/default.tsx @@ -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} - {isChallenge && !examInProgress && ( + {isChallenge && !examInProgress && isRenderBreadcrumb ? (
+ ) : isExSmallViewportHeight ? ( + + ) : ( + )} {fetchState.complete && children} diff --git a/client/src/templates/Challenges/classic/classic.css b/client/src/templates/Challenges/classic/classic.css index 8b8944ffc6a..0110c250c94 100644 --- a/client/src/templates/Challenges/classic/classic.css +++ b/client/src/templates/Challenges/classic/classic.css @@ -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; + } +} diff --git a/client/src/templates/Challenges/classic/editor-tabs.tsx b/client/src/templates/Challenges/classic/editor-tabs.tsx index 6af10cff962..3d1e659f6fc 100644 --- a/client/src/templates/Challenges/classic/editor-tabs.tsx +++ b/client/src/templates/Challenges/classic/editor-tabs.tsx @@ -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 { 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 ( -
- {/* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */} - {sortChallengeFiles(challengeFiles).map( - (challengeFile: ChallengeFile) => ( - - ) - )} -
+ isRenderChallengeFiles && ( +
+ { + /* eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call */ + sortChallengeFiles(challengeFiles).map( + (challengeFile: ChallengeFile) => ( + + ) + ) + } +
+ ) ); } } diff --git a/client/src/templates/Challenges/classic/editor.css b/client/src/templates/Challenges/classic/editor.css index 645ad78a80d..7bf459bdf1b 100644 --- a/client/src/templates/Challenges/classic/editor.css +++ b/client/src/templates/Challenges/classic/editor.css @@ -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; diff --git a/client/src/templates/Challenges/classic/editor.tsx b/client/src/templates/Challenges/classic/editor.tsx index 4f26d5fe002..5517e4559c4 100644 --- a/client/src/templates/Challenges/classic/editor.tsx +++ b/client/src/templates/Challenges/classic/editor.tsx @@ -77,6 +77,8 @@ export interface EditorProps { challengeFiles: ChallengeFiles; challengeType: number; containerRef?: React.RefObject; + block: string; + superBlock: string; description: string; dimensions?: Dimensions; editorRef: MutableRefObject; @@ -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, diff --git a/client/src/templates/Challenges/classic/multifile-editor.tsx b/client/src/templates/Challenges/classic/multifile-editor.tsx index 18f82e449c6..842c7c9066c 100644 --- a/client/src/templates/Challenges/classic/multifile-editor.tsx +++ b/client/src/templates/Challenges/classic/multifile-editor.tsx @@ -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) => { > -
    +
    1. diff --git a/e2e/bread-crumb.spec.ts b/e2e/bread-crumb.spec.ts index f519ddcc435..a8b44e3a032 100644 --- a/e2e/bread-crumb.spec.ts +++ b/e2e/bread-crumb.spec.ts @@ -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'); }); });