feat: migrate challenge modals (#54179)

Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com>
This commit is contained in:
Sem Bauke
2024-04-03 15:28:13 +03:00
committed by GitHub
co-authored by Huyen Nguyen
parent 5013ba00b1
commit a39e74052c
22 changed files with 266 additions and 304 deletions
@@ -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');
});
});
+17 -1
View File
@@ -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
View File
@@ -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 }) => {
+24 -13
View File
@@ -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
View File
@@ -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
View File
@@ -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: {
+18 -4
View File
@@ -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'
+9 -1
View File
@@ -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 {