mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-28 03:49:22 +08:00
feat: migrate challenge modals (#54179)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
co-authored by
Huyen Nguyen
parent
5013ba00b1
commit
a39e74052c
@@ -727,7 +727,9 @@
|
||||
"task": "Task",
|
||||
"dialogues-and-tasks-for": "Dialogues and tasks for {{blockTitle}}",
|
||||
"code-example": "{{codeName}} code example",
|
||||
"opens-new-window": "Opens in new window"
|
||||
"opens-new-window": "Opens in new window",
|
||||
"rsa-checkbox": "I have tried the Read-Search-Ask method",
|
||||
"similar-questions-checkbox": "I have searched for similar questions that have already been answered on the forum"
|
||||
},
|
||||
"flash": {
|
||||
"honest-first": "To claim a certification, you must first agree to our academic honesty policy",
|
||||
|
||||
@@ -1,32 +1,3 @@
|
||||
.completion-message {
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.completion-modal-body {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
}
|
||||
|
||||
.video-modal-body {
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-evenly;
|
||||
padding-inline-start: 30px;
|
||||
}
|
||||
|
||||
.completion-challenge-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.completion-success-icon {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
@@ -35,11 +6,6 @@
|
||||
animation: success-icon-animation 150ms linear 100ms forwards;
|
||||
}
|
||||
|
||||
.challenge-success-modal .btn-cta-big {
|
||||
max-width: 100%;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
@keyframes success-icon-animation {
|
||||
100% {
|
||||
opacity: 1;
|
||||
@@ -55,7 +21,6 @@
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
gap: 0.5rem;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.completion-block-name {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||
/* eslint-disable @typescript-eslint/restrict-template-expressions */
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { noop } from 'lodash-es';
|
||||
import React, { Component } from 'react';
|
||||
import type { TFunction } from 'i18next';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import Login from '../../../components/Header/components/login';
|
||||
import {
|
||||
@@ -168,30 +166,19 @@ class CompletionModal extends Component<
|
||||
|
||||
return (
|
||||
<Modal
|
||||
data-cy='completion-modal'
|
||||
animation={false}
|
||||
bsSize='lg'
|
||||
dialogClassName='challenge-success-modal'
|
||||
keyboard={true}
|
||||
onHide={close}
|
||||
onClose={close}
|
||||
open={!!isOpen}
|
||||
size='large'
|
||||
// eslint-disable-next-line @typescript-eslint/unbound-method
|
||||
onKeyDown={isOpen ? this.handleKeypress : noop}
|
||||
show={isOpen}
|
||||
onKeyDown={isOpen ? this.handleKeypress : undefined}
|
||||
>
|
||||
<Modal.Header
|
||||
className='challenge-list-header fcc-modal'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Title className='completion-message'>{message}</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='completion-modal-body'>
|
||||
<div className='completion-challenge-details'>
|
||||
<GreenPass
|
||||
className='completion-success-icon'
|
||||
data-testid='fcc-completion-success-icon'
|
||||
data-playwright-test-label='completion-success-icon'
|
||||
/>
|
||||
</div>
|
||||
<Modal.Header closeButtonClassNames='close'>{message}</Modal.Header>
|
||||
<Modal.Body>
|
||||
<GreenPass
|
||||
className='completion-success-icon'
|
||||
data-testid='fcc-completion-success-icon'
|
||||
data-playwright-test-label='completion-success-icon'
|
||||
/>
|
||||
<div className='completion-block-details'>
|
||||
<Progress />
|
||||
</div>
|
||||
|
||||
@@ -19,16 +19,12 @@
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
.checkbox-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.help-modal .btn-lg {
|
||||
font-size: 16px;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons';
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { Button, FormControl } from '@freecodecamp/ui';
|
||||
import { Button, FormControl, Modal } from '@freecodecamp/ui';
|
||||
import React, { useMemo, useState, useRef, useEffect } from 'react';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
@@ -47,34 +46,45 @@ const generateSearchLink = (title: string, block: string) => {
|
||||
|
||||
interface CheckboxProps {
|
||||
name: string;
|
||||
i18nkey: string;
|
||||
i18nKey: string;
|
||||
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
value: boolean;
|
||||
href: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function Checkbox({ name, i18nkey, onChange, value, href }: CheckboxProps) {
|
||||
function Checkbox({
|
||||
name,
|
||||
i18nKey,
|
||||
onChange,
|
||||
value,
|
||||
href,
|
||||
label
|
||||
}: CheckboxProps) {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<div className='checkbox'>
|
||||
<label>
|
||||
<input
|
||||
name={name}
|
||||
type='checkbox'
|
||||
onChange={onChange}
|
||||
checked={value}
|
||||
required
|
||||
/>
|
||||
<span className='checkbox-text'>
|
||||
<Trans i18nKey={i18nkey}>
|
||||
<a href={href} rel='noopener noreferrer' target='_blank'>
|
||||
placeholder
|
||||
<span className='sr-only'>{t('aria.opens-new-window')}</span>
|
||||
</a>
|
||||
</Trans>
|
||||
</span>
|
||||
</label>
|
||||
<div className='checkbox-container'>
|
||||
<input
|
||||
id={name}
|
||||
name={name}
|
||||
type='checkbox'
|
||||
onChange={onChange}
|
||||
checked={value}
|
||||
required
|
||||
// Instead of reusing the `i18nKey`, use a plain text version for label
|
||||
// as input label should not contain interactive elements
|
||||
aria-label={t(label)}
|
||||
/>
|
||||
|
||||
<span>
|
||||
<Trans i18nKey={i18nKey}>
|
||||
<a href={href} rel='noopener noreferrer' target='_blank'>
|
||||
placeholder
|
||||
<span className='sr-only'>{t('aria.opens-new-window')}</span>
|
||||
</a>
|
||||
</Trans>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -138,22 +148,11 @@ function HelpModal({
|
||||
callGA({ event: 'pageview', pagePath: '/help-modal' });
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='help-modal'
|
||||
onHide={handleClose}
|
||||
show={isOpen}
|
||||
aria-labelledby='ask-for-help-modal'
|
||||
>
|
||||
<Modal.Header
|
||||
className='help-modal-header fcc-modal'
|
||||
closeButton={true}
|
||||
closeLabel={t('buttons.close')}
|
||||
>
|
||||
<Modal.Title id='ask-for-help-modal' className='text-center'>
|
||||
{t('buttons.ask-for-help')}
|
||||
</Modal.Title>
|
||||
<Modal onClose={handleClose} open={!!isOpen}>
|
||||
<Modal.Header closeButtonClassNames='close'>
|
||||
{t('buttons.ask-for-help')}
|
||||
</Modal.Header>
|
||||
<Modal.Body className='text-center'>
|
||||
<Modal.Body>
|
||||
{showHelpForm ? (
|
||||
<form onSubmit={handleSubmit} ref={formRef}>
|
||||
<fieldset>
|
||||
@@ -163,7 +162,8 @@ function HelpModal({
|
||||
|
||||
<Checkbox
|
||||
name='read-search-ask-checkbox'
|
||||
i18nkey='learn.read-search-ask-checkbox'
|
||||
i18nKey='learn.read-search-ask-checkbox'
|
||||
label='aria.rsa-checkbox'
|
||||
onChange={event => setReadSearchCheckbox(event.target.checked)}
|
||||
value={readSearchCheckbox}
|
||||
href={RSA}
|
||||
@@ -173,7 +173,8 @@ function HelpModal({
|
||||
|
||||
<Checkbox
|
||||
name='similar-questions-checkbox'
|
||||
i18nkey='learn.similar-questions-checkbox'
|
||||
i18nKey='learn.similar-questions-checkbox'
|
||||
label='aria.similar-questions-checkbox'
|
||||
onChange={event =>
|
||||
setSimilarQuestionsCheckbox(event.target.checked)
|
||||
}
|
||||
|
||||
@@ -20,23 +20,39 @@ import {
|
||||
import {
|
||||
canFocusEditorSelector,
|
||||
challengeFilesSelector,
|
||||
challengeTestsSelector
|
||||
challengeTestsSelector,
|
||||
isHelpModalOpenSelector,
|
||||
isProjectPreviewModalOpenSelector,
|
||||
isResetModalOpenSelector,
|
||||
isShortcutsModalOpenSelector
|
||||
} from '../redux/selectors';
|
||||
import './hotkeys.css';
|
||||
import { isProjectBased } from '../../../utils/curriculum-layout';
|
||||
import type { EditorProps } from '../classic/editor';
|
||||
|
||||
const mapStateToProps = createSelector(
|
||||
isHelpModalOpenSelector,
|
||||
isResetModalOpenSelector,
|
||||
isShortcutsModalOpenSelector,
|
||||
isProjectPreviewModalOpenSelector,
|
||||
canFocusEditorSelector,
|
||||
challengeFilesSelector,
|
||||
challengeTestsSelector,
|
||||
userSelector,
|
||||
(
|
||||
isHelpModalOpen: boolean,
|
||||
isResetModalOpen: boolean,
|
||||
isShortcutsModalOpen: boolean,
|
||||
isProjectPreviewModalOpen: boolean,
|
||||
canFocusEditor: boolean,
|
||||
challengeFiles: ChallengeFiles,
|
||||
tests: Test[],
|
||||
user: User
|
||||
) => ({
|
||||
isHelpModalOpen,
|
||||
isResetModalOpen,
|
||||
isShortcutsModalOpen,
|
||||
isProjectPreviewModalOpen,
|
||||
canFocusEditor,
|
||||
challengeFiles,
|
||||
tests,
|
||||
@@ -51,16 +67,6 @@ const mapDispatchToProps = {
|
||||
setIsAdvancing
|
||||
};
|
||||
|
||||
const keyMap = {
|
||||
navigationMode: 'escape',
|
||||
executeChallenge: ['ctrl+enter', 'command+enter'],
|
||||
focusEditor: 'e',
|
||||
focusInstructionsPanel: 'r',
|
||||
navigatePrev: ['p'],
|
||||
navigateNext: ['n'],
|
||||
showShortcuts: 'shift+/'
|
||||
};
|
||||
|
||||
export type HotkeysProps = Pick<
|
||||
ChallengeMeta,
|
||||
'nextChallengePath' | 'prevChallengePath'
|
||||
@@ -79,6 +85,10 @@ export type HotkeysProps = Pick<
|
||||
| 'submitChallenge'
|
||||
| 'setEditorFocusability'
|
||||
> & {
|
||||
isHelpModalOpen?: boolean;
|
||||
isResetModalOpen?: boolean;
|
||||
isShortcutsModalOpen?: boolean;
|
||||
isProjectPreviewModalOpen?: boolean;
|
||||
canFocusEditor: boolean;
|
||||
children: React.ReactElement;
|
||||
instructionsPanelRef?: React.RefObject<HTMLElement>;
|
||||
@@ -104,8 +114,32 @@ function Hotkeys({
|
||||
tests,
|
||||
usesMultifileEditor,
|
||||
openShortcutsModal,
|
||||
user: { keyboardShortcuts }
|
||||
user: { keyboardShortcuts },
|
||||
isHelpModalOpen,
|
||||
isResetModalOpen,
|
||||
isShortcutsModalOpen,
|
||||
isProjectPreviewModalOpen
|
||||
}: HotkeysProps): JSX.Element {
|
||||
const isModalOpen = [
|
||||
isHelpModalOpen,
|
||||
isResetModalOpen,
|
||||
isShortcutsModalOpen,
|
||||
isProjectPreviewModalOpen
|
||||
].some(Boolean);
|
||||
|
||||
const keyMap = {
|
||||
// The Modal component needs to listen to the 'Escape' keypress event
|
||||
// in order to close itself when the key is press.
|
||||
// Therefore, we don't want HotKeys to hijack the 'escape' event when a modal is open.
|
||||
navigationMode: isModalOpen ? '' : 'escape',
|
||||
executeChallenge: ['ctrl+enter', 'command+enter'],
|
||||
focusEditor: 'e',
|
||||
focusInstructionsPanel: 'r',
|
||||
navigatePrev: ['p'],
|
||||
navigateNext: ['n'],
|
||||
showShortcuts: 'shift+/'
|
||||
};
|
||||
|
||||
const handlers = {
|
||||
executeChallenge: (keyEvent?: KeyboardEvent) => {
|
||||
// the 'enter' part of 'ctrl+enter' stops HotKeys from listening, so it
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React, { useEffect } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import type { CompletedChallenge } from '../../../redux/prop-types';
|
||||
import {
|
||||
@@ -56,28 +55,15 @@ function ProjectPreviewModal({
|
||||
|
||||
return (
|
||||
<Modal
|
||||
data-playwright-test-label='project-preview-modal'
|
||||
bsSize='lg'
|
||||
data-cy='project-preview-modal'
|
||||
dialogClassName='project-preview-modal'
|
||||
onHide={() => {
|
||||
size='large'
|
||||
onClose={() => {
|
||||
closeModal('projectPreview');
|
||||
setEditorFocusability(true);
|
||||
}}
|
||||
show={isOpen}
|
||||
open={isOpen}
|
||||
>
|
||||
<Modal.Header
|
||||
className='project-preview-modal-header fcc-modal'
|
||||
closeButton={true}
|
||||
>
|
||||
<Modal.Title
|
||||
className='text-center'
|
||||
data-playwright-test-label='project-preview-modal-title'
|
||||
>
|
||||
{previewTitle}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body className='project-preview-modal-body text-center'>
|
||||
<Modal.Header closeButtonClassNames='close'>{previewTitle}</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Preview
|
||||
previewId={projectPreviewId}
|
||||
previewMounted={() =>
|
||||
@@ -90,7 +76,6 @@ function ProjectPreviewModal({
|
||||
block={true}
|
||||
size='large'
|
||||
variant='primary'
|
||||
data-playwright-test-label='project-preview-modal-closeButton'
|
||||
onClick={() => {
|
||||
closeModal('projectPreview');
|
||||
setEditorFocusability(true);
|
||||
|
||||
@@ -1,29 +1,23 @@
|
||||
.shortcuts-modal-body table {
|
||||
.shortcuts-modal-table {
|
||||
width: 100%;
|
||||
margin-bottom: 2rem !important;
|
||||
}
|
||||
|
||||
.shortcuts-modal-body table caption {
|
||||
.shortcuts-modal-table caption {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shortcuts-modal-body table th {
|
||||
.shortcuts-modal-table th {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.shortcuts-modal-body table tbody tr {
|
||||
.shortcuts-modal-table tbody tr {
|
||||
line-height: 2rem;
|
||||
border-bottom: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.shortcuts-modal-body table tbody tr td:nth-child(2) {
|
||||
.shortcuts-modal-table tbody tr td:nth-child(2) {
|
||||
text-align: right;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 767px) {
|
||||
.help-modal .btn-lg {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React from 'react';
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { createSelector } from 'reselect';
|
||||
import { Button } from '@freecodecamp/ui';
|
||||
import { Button, Modal } from '@freecodecamp/ui';
|
||||
|
||||
import { closeModal } from '../redux/actions';
|
||||
import { isShortcutsModalOpenSelector } from '../redux/selectors';
|
||||
@@ -46,23 +45,12 @@ function ShortcutsModal({
|
||||
user: { keyboardShortcuts }
|
||||
}: ShortcutsModalProps): JSX.Element {
|
||||
return (
|
||||
<Modal
|
||||
dialogClassName='shortcuts-modal'
|
||||
onHide={closeShortcutsModal}
|
||||
show={isOpen}
|
||||
aria-labelledby='shortcuts-modal-title'
|
||||
>
|
||||
<Modal.Header className='shortcuts-modal-header fcc-modal'>
|
||||
<Modal.Title
|
||||
id='shortcuts-modal-title'
|
||||
className='text-center'
|
||||
componentClass='h1'
|
||||
>
|
||||
{t('shortcuts.title')}
|
||||
</Modal.Title>
|
||||
<Modal onClose={closeShortcutsModal} open={isOpen}>
|
||||
<Modal.Header showCloseButton={false}>
|
||||
{t('shortcuts.title')}
|
||||
</Modal.Header>
|
||||
<Modal.Body className='shortcuts-modal-body'>
|
||||
<table>
|
||||
<Modal.Body alignment='start'>
|
||||
<table className='shortcuts-modal-table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope='col'>{t('shortcuts.table-header-action')}</th>
|
||||
|
||||
@@ -1,32 +1,14 @@
|
||||
.video-modal {
|
||||
height: calc(100vh - 20px);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.video-modal {
|
||||
width: 90%;
|
||||
height: calc(100vh - 60px);
|
||||
}
|
||||
}
|
||||
|
||||
.video-modal .modal-content {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.video-modal-body {
|
||||
flex-grow: 1;
|
||||
height: 85vh;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.video-modal iframe {
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
.video-modal-body iframe {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.video-modal p {
|
||||
.video-modal-body p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
||||
import React from 'react';
|
||||
import { withTranslation } from 'react-i18next';
|
||||
import { connect } from 'react-redux';
|
||||
import { bindActionCreators, Dispatch } from 'redux';
|
||||
import { Modal } from '@freecodecamp/ui';
|
||||
import { closeModal } from '../redux/actions';
|
||||
import { isVideoModalOpenSelector } from '../redux/selectors';
|
||||
import callGA from '../../../analytics/call-ga';
|
||||
@@ -11,7 +11,7 @@ import './video-modal.css';
|
||||
|
||||
interface VideoModalProps {
|
||||
closeVideoModal: () => void;
|
||||
isOpen?: boolean;
|
||||
isOpen: boolean;
|
||||
t: (attribute: string) => string;
|
||||
videoUrl?: string;
|
||||
}
|
||||
@@ -33,19 +33,13 @@ function VideoModal({
|
||||
callGA({ event: 'pageview', pagePath: '/completion-modal' });
|
||||
}
|
||||
return (
|
||||
<Modal dialogClassName='video-modal' onHide={closeVideoModal} show={isOpen}>
|
||||
<Modal.Header className='video-modal-header fcc-modal' closeButton={true}>
|
||||
<Modal.Title
|
||||
className='text-center'
|
||||
data-playwright-test-label='video-modal-title'
|
||||
>
|
||||
{t('buttons.watch-video')}
|
||||
</Modal.Title>
|
||||
<Modal onClose={closeVideoModal} size='xLarge' open={isOpen}>
|
||||
<Modal.Header closeButtonClassNames='close'>
|
||||
{t('buttons.watch-video')}
|
||||
</Modal.Header>
|
||||
<Modal.Body className='video-modal-body'>
|
||||
<Modal.Body alignment='start' className='video-modal-body'>
|
||||
<iframe
|
||||
data-playwright-test-label='video-modal-video-player-iframe'
|
||||
frameBorder='0'
|
||||
src={videoUrl}
|
||||
title={t('buttons.watch-video')}
|
||||
/>
|
||||
|
||||
@@ -31,7 +31,9 @@ describe('CodeAlly cert challenge', function () {
|
||||
.type('https://example.com')
|
||||
.type('{enter}');
|
||||
|
||||
cy.get('.completion-modal-body');
|
||||
cy.get('div[role="dialog"]')
|
||||
.contains('Submit and go to next challenge')
|
||||
.should('be.visible');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -72,13 +72,11 @@ const legacyFirstChallengeUrls = [
|
||||
|
||||
describe('project preview', () => {
|
||||
it('should appear on the first challenges of each practice project', () => {
|
||||
practiceProjects.forEach(({ url, title }) => {
|
||||
practiceProjects.forEach(({ url }) => {
|
||||
cy.visit(url + 'step-1');
|
||||
cy.contains("Here's a preview of what you will build");
|
||||
cy.get('[data-cy="project-preview-modal"]')
|
||||
.should('be.focused')
|
||||
.find('iframe')
|
||||
.should('have.attr', 'title', title + ' preview');
|
||||
cy.get('div[role="dialog"]')
|
||||
.contains("Here's a preview of what you will build")
|
||||
.should('be.visible');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -250,12 +250,10 @@ describe('project submission', () => {
|
||||
.type('https://replit.com/@camperbot/python-project#main.py');
|
||||
|
||||
cy.contains("I've completed this challenge").click();
|
||||
cy.get('[data-cy=completion-modal]').should('exist');
|
||||
cy.get('div[role="dialog"]').should('exist');
|
||||
cy.get('[data-cy=submit-challenge]').as('submitChallenge');
|
||||
cy.get('@submitChallenge').click();
|
||||
cy.get('@submitChallenge').should('have.attr', 'aria-disabled');
|
||||
// After the api responds, the button is enabled, but since the modal leaves
|
||||
// the DOM we just check for that.
|
||||
cy.get('[data-cy=completion-modal]').should('not.exist');
|
||||
cy.get('div[role="dialog"]').should('not.exist');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -69,10 +69,26 @@ test.describe('Challenge Completion Modal Tests (Signed In)', () => {
|
||||
await expect(page.getByTestId('completion-success-icon')).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('should redirect to next challenge', async ({ page }) => {
|
||||
test('should submit and go to the next challenge when the user clicks the submit button', async ({
|
||||
page
|
||||
}) => {
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons['submit-and-go'] })
|
||||
.click();
|
||||
await expect(page).toHaveURL(nextChallengeURL);
|
||||
});
|
||||
|
||||
test('should submit and go to the next challenge when the user presses Ctrl + Enter', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.keyboard.press('Control+Enter');
|
||||
await expect(page).toHaveURL(nextChallengeURL);
|
||||
});
|
||||
|
||||
test('should submit and go to the next challenge when the user presses Command + Enter', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.keyboard.press('Meta+Enter');
|
||||
await expect(page).toHaveURL(nextChallengeURL);
|
||||
});
|
||||
});
|
||||
|
||||
+20
-30
@@ -16,10 +16,7 @@ test.describe('Help Modal component', () => {
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.buttons['ask-for-help'],
|
||||
exact: true
|
||||
})
|
||||
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
@@ -57,10 +54,7 @@ test.describe('Help Modal component', () => {
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.buttons['ask-for-help'],
|
||||
exact: true
|
||||
})
|
||||
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
|
||||
).toBeVisible();
|
||||
|
||||
const rsaCheckbox = page.getByRole('checkbox', {
|
||||
@@ -110,10 +104,7 @@ test.describe('Help Modal component', () => {
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.buttons['ask-for-help'],
|
||||
exact: true
|
||||
})
|
||||
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
|
||||
).toBeVisible();
|
||||
|
||||
const rsaCheckbox = page.getByRole('checkbox', {
|
||||
@@ -132,8 +123,8 @@ test.describe('Help Modal component', () => {
|
||||
name: translations.buttons['submit']
|
||||
});
|
||||
|
||||
await rsaCheckbox.click();
|
||||
await similarQuestionsCheckbox.click();
|
||||
await rsaCheckbox.check();
|
||||
await similarQuestionsCheckbox.check();
|
||||
await descriptionInput.fill('Example text');
|
||||
|
||||
await expect(submitButton).toBeDisabled();
|
||||
@@ -154,10 +145,7 @@ test.describe('Help Modal component', () => {
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.buttons['ask-for-help'],
|
||||
exact: true
|
||||
})
|
||||
page.getByRole('dialog', { name: translations.buttons['ask-for-help'] })
|
||||
).toBeVisible();
|
||||
|
||||
const rsaCheckbox = page.getByRole('checkbox', {
|
||||
@@ -202,16 +190,17 @@ test.describe('Help Modal component', () => {
|
||||
.getByRole('button', { name: translations.buttons['ask-for-help'] })
|
||||
.click();
|
||||
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.buttons['ask-for-help']
|
||||
});
|
||||
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons.cancel })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.buttons['ask-for-help'],
|
||||
exact: true
|
||||
})
|
||||
).not.toBeVisible();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Close button closes the modal', async ({ page }) => {
|
||||
@@ -219,16 +208,17 @@ test.describe('Help Modal component', () => {
|
||||
.getByRole('button', { name: translations.buttons['ask-for-help'] })
|
||||
.click();
|
||||
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.buttons['ask-for-help']
|
||||
});
|
||||
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons.close })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', {
|
||||
name: translations.buttons['ask-for-help'],
|
||||
exact: true
|
||||
})
|
||||
).not.toBeVisible();
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Read-Search-Ask link', async ({ page }) => {
|
||||
|
||||
@@ -18,33 +18,44 @@ test.describe('Exit Project Preview Modal E2E Test Suite', () => {
|
||||
name: translations.buttons.close
|
||||
})
|
||||
).toBeVisible();
|
||||
await expect(page.getByTestId('project-preview-modal-title')).toBeVisible();
|
||||
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.learn['project-preview-title']
|
||||
});
|
||||
await expect(dialog).toBeVisible();
|
||||
await expect(dialog.getByTitle('CatPhotoApp preview')).toBeVisible();
|
||||
await expect(
|
||||
page.locator('div.modal-body').getByTestId('preview-iframe')
|
||||
).toBeVisible();
|
||||
// JS will generate 2 iframes, the right one should be chosen
|
||||
await expect(
|
||||
page.getByTestId('project-preview-modal-closeButton')
|
||||
page.getByRole('button', { name: 'Start Coding!' })
|
||||
).toBeVisible();
|
||||
});
|
||||
|
||||
test('Closes the Project Preview Modal When the User clicks on the close Button', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.getByTestId('project-preview-modal-closeButton').click();
|
||||
await expect(
|
||||
page.getByTestId('project-preview-modal-title')
|
||||
).not.toBeVisible();
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.learn['project-preview-title']
|
||||
});
|
||||
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: 'Start Coding!' }).click();
|
||||
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('Closes the Project Preview Modal when the User clicks on X button', async ({
|
||||
page
|
||||
}) => {
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.learn['project-preview-title']
|
||||
});
|
||||
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons.close })
|
||||
.click();
|
||||
await expect(
|
||||
page.getByTestId('project-preview-modal-title')
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
+20
-30
@@ -39,16 +39,12 @@ test('User can see list of shortcuts by pressing SHIFT + ?', async ({
|
||||
'Skipping on mobile as it does not have a physical keyboard.'
|
||||
);
|
||||
|
||||
const dialogs = page.getByRole('dialog');
|
||||
await expect(dialogs).toHaveCount(2);
|
||||
|
||||
await expect(
|
||||
page.getByRole('heading', { name: translations.shortcuts.title })
|
||||
page.getByRole('dialog', { name: translations.shortcuts.title })
|
||||
).toBeVisible();
|
||||
|
||||
for (const shortcut of Object.values(translations.shortcuts)) {
|
||||
if (shortcut === translations.shortcuts.title) continue;
|
||||
|
||||
await expect(page.getByText(shortcut)).toBeVisible();
|
||||
}
|
||||
|
||||
@@ -72,19 +68,16 @@ test('User can close the modal by pressing ESC', async ({ page, isMobile }) => {
|
||||
isMobile,
|
||||
'Skipping on mobile as it does not have a physical keyboard.'
|
||||
);
|
||||
const dialogs = page.getByRole('dialog');
|
||||
await expect(dialogs).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: translations.shortcuts.title })
|
||||
).toBeVisible();
|
||||
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.shortcuts.title
|
||||
});
|
||||
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Escape');
|
||||
for (const dialog of await dialogs.all()) {
|
||||
await expect(dialog).not.toBeVisible();
|
||||
}
|
||||
await expect(
|
||||
page.getByRole('heading', { name: translations.shortcuts.title })
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
test('User can disable keyboard shortcuts', async ({ page, isMobile }) => {
|
||||
@@ -92,26 +85,23 @@ test('User can disable keyboard shortcuts', async ({ page, isMobile }) => {
|
||||
isMobile,
|
||||
'Skipping on mobile as it does not have a physical keyboard.'
|
||||
);
|
||||
const dialogs = page.getByRole('dialog');
|
||||
await expect(dialogs).toHaveCount(2);
|
||||
await expect(
|
||||
page.getByRole('heading', { name: translations.shortcuts.title })
|
||||
).toBeVisible();
|
||||
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.shortcuts.title
|
||||
});
|
||||
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page.getByRole('button', { name: translations.buttons.off }).click();
|
||||
await page.getByRole('button', { name: translations.buttons.close }).click();
|
||||
for (const dialog of await dialogs.all()) {
|
||||
await expect(dialog).not.toBeVisible();
|
||||
}
|
||||
await expect(
|
||||
page.getByRole('heading', { name: translations.shortcuts.title })
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(dialog).not.toBeVisible();
|
||||
|
||||
await expect(
|
||||
page.getByText(new RegExp(translations.flash['keyboard-shortcut-updated']))
|
||||
).toBeVisible();
|
||||
|
||||
await page.keyboard.press('Shift+?');
|
||||
await expect(
|
||||
page.getByRole('heading', { name: translations.shortcuts.title })
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
|
||||
+11
-11
@@ -14,9 +14,9 @@ test.describe('Exit Video Modal E2E Test Suite', () => {
|
||||
test('Verifies the Correct Rendering of the Video Modal', async ({
|
||||
page
|
||||
}) => {
|
||||
const dialogs = page.getByRole('dialog');
|
||||
await expect(dialogs).toHaveCount(2);
|
||||
await expect(page.getByTestId('video-modal-title')).toBeVisible();
|
||||
await expect(
|
||||
page.getByRole('dialog', { name: translations.buttons['watch-video'] })
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByTestId('video-modal-video-player-iframe')
|
||||
).toBeVisible();
|
||||
@@ -31,16 +31,16 @@ test.describe('Exit Video Modal E2E Test Suite', () => {
|
||||
test('Closes the Video Modal When the User clicks on exit button', async ({
|
||||
page
|
||||
}) => {
|
||||
const dialogs = page.getByRole('dialog');
|
||||
await expect(dialogs).toHaveCount(2);
|
||||
const dialog = page.getByRole('dialog', {
|
||||
name: translations.buttons['watch-video']
|
||||
});
|
||||
|
||||
await expect(dialog).toBeVisible();
|
||||
|
||||
await page
|
||||
.getByRole('button', { name: translations.buttons.close })
|
||||
.click();
|
||||
for (const dialog of await dialogs.all()) {
|
||||
await expect(dialog).not.toBeVisible();
|
||||
}
|
||||
await expect(
|
||||
page.getByText(translations.buttons['watch-video'])
|
||||
).not.toBeVisible();
|
||||
|
||||
await expect(dialog).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ const story = {
|
||||
control: false
|
||||
},
|
||||
size: {
|
||||
options: ['medium', 'large']
|
||||
options: ['medium', 'large', 'xLarge']
|
||||
},
|
||||
variant: {
|
||||
options: ['default', 'danger']
|
||||
@@ -140,6 +140,13 @@ export const Large: Story = {
|
||||
}
|
||||
};
|
||||
|
||||
export const ExtraLarge: Story = {
|
||||
render: DefaultTemplate,
|
||||
args: {
|
||||
size: 'xLarge'
|
||||
}
|
||||
};
|
||||
|
||||
export const Danger: Story = {
|
||||
render: DangerTemplate,
|
||||
args: {
|
||||
|
||||
@@ -65,13 +65,18 @@ const Header = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Body = ({ children, alignment = 'center', borderless }: BodyProps) => {
|
||||
const Body = ({
|
||||
children,
|
||||
alignment = 'center',
|
||||
borderless,
|
||||
className
|
||||
}: BodyProps) => {
|
||||
const borderClasses = borderless
|
||||
? ''
|
||||
: 'border-b-1 border-solid border-foreground-secondary';
|
||||
|
||||
return (
|
||||
<div className={`p-[15px] text-${alignment} ${borderClasses}`}>
|
||||
<div className={`p-[15px] text-${alignment} ${borderClasses} ${className}`}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
@@ -91,6 +96,7 @@ const Modal = ({
|
||||
children,
|
||||
open,
|
||||
onClose,
|
||||
onKeyDown,
|
||||
size = 'medium',
|
||||
variant = 'default'
|
||||
}: ModalProps) => {
|
||||
@@ -100,6 +106,8 @@ const Modal = ({
|
||||
panelClasses = panelClasses.concat(' ', 'w-[600px]');
|
||||
} else if (size === 'large') {
|
||||
panelClasses = panelClasses.concat(' ', 'w-[900px]');
|
||||
} else if (size === 'xLarge') {
|
||||
panelClasses = panelClasses.concat(' ', 'w-[95vw] md:w-[90vw]');
|
||||
}
|
||||
|
||||
if (variant === 'default') {
|
||||
@@ -111,12 +119,18 @@ const Modal = ({
|
||||
return (
|
||||
<ModalContext.Provider value={{ onClose, variant }}>
|
||||
<Transition.Root show={open} as={Fragment}>
|
||||
<Dialog onClose={onClose} className='relative z-1050 w-screen h-screen'>
|
||||
<Dialog
|
||||
onClose={onClose}
|
||||
className='relative z-1050 w-screen h-screen'
|
||||
onKeyDown={onKeyDown}
|
||||
>
|
||||
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
||||
<div aria-hidden className='fixed inset-0 bg-gray-900 opacity-50' />
|
||||
|
||||
{/* Full-screen container of the panel */}
|
||||
<div className='fixed inset-0 flex items-start justify-center pt-[30px] pb-[30px] overflow-scroll'>
|
||||
<div
|
||||
className={`fixed inset-0 flex items-start justify-center p-[10px] md:pt-[30px] md:pb-[30px] overflow-scroll`}
|
||||
>
|
||||
<Transition.Child
|
||||
as={Fragment}
|
||||
enter='transition-all duration-300 ease-out'
|
||||
|
||||
@@ -4,7 +4,8 @@ export interface ModalProps {
|
||||
children: ReactNode;
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
size?: 'large' | 'medium';
|
||||
onKeyDown?: React.KeyboardEventHandler;
|
||||
size?: 'medium' | 'large' | 'xLarge';
|
||||
variant?: 'default' | 'danger';
|
||||
}
|
||||
|
||||
@@ -32,7 +33,14 @@ export interface BodyProps {
|
||||
* Use `left` if the modal body contains code blocks.
|
||||
*/
|
||||
alignment?: 'center' | 'left' | 'start';
|
||||
|
||||
borderless?: boolean;
|
||||
|
||||
/**
|
||||
* This is an escape hatch for cases where the component doesn't meet your design requirements.
|
||||
* Use this option sparingly.
|
||||
*/
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export interface FooterProps {
|
||||
|
||||
Reference in New Issue
Block a user