mirror of
https://github.com/freeCodeCamp/freeCodeCamp.git
synced 2026-10-29 18:14:34 +08:00
feat: migrate setting modals (#54112)
Co-authored-by: Huyen Nguyen <25715018+huyenltnguyen@users.noreply.github.com> Co-authored-by: ahmad abdolsaheb <ahmad.abdolsaheb@gmail.com>
This commit is contained in:
co-authored by
Huyen Nguyen
ahmad abdolsaheb
parent
f6ae52f6fe
commit
76b4c812cb
@@ -1,8 +1,7 @@
|
|||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Row, Button } from '@freecodecamp/ui';
|
import { Row, Button, Modal } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import type { GeneratedExamResults } from '../../redux/prop-types';
|
import type { GeneratedExamResults } from '../../redux/prop-types';
|
||||||
import { closeModal } from '../../templates/Challenges/redux/actions';
|
import { closeModal } from '../../templates/Challenges/redux/actions';
|
||||||
@@ -51,36 +50,36 @@ const ExamResultsModal = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
aria-labelledby='solution-viewer-modal-title'
|
onClose={() => {
|
||||||
bsSize='large'
|
|
||||||
onHide={() => {
|
|
||||||
closeModal('examResults');
|
closeModal('examResults');
|
||||||
}}
|
}}
|
||||||
show={isOpen}
|
open={isOpen}
|
||||||
size='lg'
|
size='large'
|
||||||
>
|
>
|
||||||
<Modal.Header closeButton={true}>
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
<Modal.Title id='solution-viewer-modal-title'>
|
{t('settings.labels.results-for', { projectTitle })}
|
||||||
{t('settings.labels.results-for', { projectTitle })}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body style={{ paddingLeft: 30 }}>
|
<Modal.Body>
|
||||||
<Spacer size='medium' />
|
<Spacer size='medium' />
|
||||||
<Row>
|
<div style={{ paddingLeft: '30px' }}>
|
||||||
{t('learn.exam.number-of-questions', {
|
<Row>
|
||||||
n: numberOfQuestionsInExam
|
{t('learn.exam.number-of-questions', {
|
||||||
})}
|
n: numberOfQuestionsInExam
|
||||||
</Row>{' '}
|
})}
|
||||||
<Spacer size='medium' />
|
</Row>{' '}
|
||||||
<Row>
|
<Spacer size='medium' />
|
||||||
{t('learn.exam.correct-answers', { n: numberOfCorrectAnswers })}
|
<Row>
|
||||||
</Row>{' '}
|
{t('learn.exam.correct-answers', { n: numberOfCorrectAnswers })}
|
||||||
<Spacer size='medium' />
|
</Row>{' '}
|
||||||
<Row>{t('learn.exam.percent-correct', { n: percentCorrect })}</Row>
|
<Spacer size='medium' />
|
||||||
<Spacer size='medium' />{' '}
|
<Row>{t('learn.exam.percent-correct', { n: percentCorrect })}</Row>
|
||||||
<Row>
|
<Spacer size='medium' />{' '}
|
||||||
{t('learn.exam.time', { t: formatSecondsToTime(examTimeInSeconds) })}
|
<Row>
|
||||||
</Row>
|
{t('learn.exam.time', {
|
||||||
|
t: formatSecondsToTime(examTimeInSeconds)
|
||||||
|
})}
|
||||||
|
</Row>
|
||||||
|
</div>
|
||||||
<Spacer size='medium' />
|
<Spacer size='medium' />
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
import { Button, Modal } from '@freecodecamp/ui';
|
||||||
import { Button } from '@freecodecamp/ui';
|
|
||||||
|
|
||||||
import { CompletedChallenge } from '../../redux/prop-types';
|
import { CompletedChallenge } from '../../redux/prop-types';
|
||||||
import SolutionViewer from './solution-viewer';
|
import SolutionViewer from './solution-viewer';
|
||||||
@@ -23,23 +22,14 @@ const ProjectModal = ({
|
|||||||
}: ProjectModalProps): JSX.Element => {
|
}: ProjectModalProps): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal onClose={handleSolutionModalHide} open={isOpen} size='large'>
|
||||||
data-playwright-test-label='project-solution-viewer-modal'
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
aria-labelledby='solution-viewer-modal-title'
|
{t('settings.labels.solution-for', { projectTitle })}
|
||||||
bsSize='large'
|
|
||||||
onHide={handleSolutionModalHide}
|
|
||||||
show={isOpen}
|
|
||||||
size='lg'
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title id='solution-viewer-modal-title'>
|
|
||||||
{t('settings.labels.solution-for', { projectTitle })}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body alignment='left'>
|
||||||
<SolutionViewer challengeFiles={challengeFiles} solution={solution} />
|
<SolutionViewer challengeFiles={challengeFiles} solution={solution} />
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer>
|
<Modal.Footer alignment='end'>
|
||||||
<Button
|
<Button
|
||||||
data-cy='solution-viewer-close-btn'
|
data-cy='solution-viewer-close-btn'
|
||||||
onClick={handleSolutionModalHide}
|
onClick={handleSolutionModalHide}
|
||||||
|
|||||||
@@ -576,3 +576,14 @@ blockquote .small {
|
|||||||
color: white;
|
color: white;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* /learn sets some default styles to all `h2`s.
|
||||||
|
* These rules are to override the defaults and apply danger styles to `danger` modal.
|
||||||
|
*/
|
||||||
|
#headlessui-portal-root h2 {
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
#headlessui-portal-root .text-background-danger h2 {
|
||||||
|
color: var(--background-danger);
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ import React, { useMemo, useState } from 'react';
|
|||||||
import type { TFunction } from 'i18next';
|
import type { TFunction } from 'i18next';
|
||||||
import { withTranslation } from 'react-i18next';
|
import { withTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
import { Table, Button, Modal } from '@freecodecamp/ui';
|
||||||
import { Table, Button } from '@freecodecamp/ui';
|
|
||||||
|
|
||||||
import envData from '../../../../config/env.json';
|
import envData from '../../../../config/env.json';
|
||||||
import { getLangCode } from '../../../../../shared/config/i18n';
|
import { getLangCode } from '../../../../../shared/config/i18n';
|
||||||
@@ -197,17 +196,11 @@ function TimelineInner({
|
|||||||
</Table>
|
</Table>
|
||||||
)}
|
)}
|
||||||
{id && (
|
{id && (
|
||||||
<Modal
|
<Modal onClose={closeSolution} open={solutionOpen}>
|
||||||
aria-labelledby='contained-modal-title'
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
onHide={closeSolution}
|
{`${username}'s Solution to ${
|
||||||
show={solutionOpen}
|
idToNameMap.get(id)?.challengeTitle ?? ''
|
||||||
>
|
}`}
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title id='contained-modal-title' className='text-center'>
|
|
||||||
{`${username}'s Solution to ${
|
|
||||||
idToNameMap.get(id)?.challengeTitle ?? ''
|
|
||||||
}`}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<SolutionViewer
|
<SolutionViewer
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@freecodecamp/ui';
|
import { Button, Modal } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import { Spacer } from '../helpers';
|
import { Spacer } from '../helpers';
|
||||||
|
|
||||||
@@ -16,19 +15,9 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
|
|||||||
const email = 'support@freecodecamp.org';
|
const email = 'support@freecodecamp.org';
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal onClose={onHide} open={show} variant='danger' size='large'>
|
||||||
aria-labelledby='modal-title'
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
backdrop={true}
|
{t('settings.danger.delete-title')}
|
||||||
bsSize='lg'
|
|
||||||
className='text-center'
|
|
||||||
keyboard={true}
|
|
||||||
onHide={onHide}
|
|
||||||
show={show}
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title id='modal-title'>
|
|
||||||
{t('settings.danger.delete-title')}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>{t('settings.danger.delete-p1')}</p>
|
<p>{t('settings.danger.delete-p1')}</p>
|
||||||
@@ -40,7 +29,8 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
|
|||||||
</a>
|
</a>
|
||||||
</Trans>
|
</Trans>
|
||||||
</p>
|
</p>
|
||||||
<hr />
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
size='large'
|
size='large'
|
||||||
@@ -60,9 +50,6 @@ function DeleteModal(props: DeleteModalProps): JSX.Element {
|
|||||||
>
|
>
|
||||||
{t('settings.danger.certain')}
|
{t('settings.danger.certain')}
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button onClick={props.onHide}>{t('buttons.close')}</Button>
|
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@freecodecamp/ui';
|
import { Button, Modal } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import { Spacer } from '../helpers';
|
import { Spacer } from '../helpers';
|
||||||
|
|
||||||
@@ -16,24 +15,15 @@ function ResetModal(props: ResetModalProps): JSX.Element {
|
|||||||
const { show, onHide } = props;
|
const { show, onHide } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal size='large' onClose={onHide} variant='danger' open={show}>
|
||||||
aria-labelledby='modal-title'
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
backdrop={true}
|
{t('settings.danger.reset-heading')}
|
||||||
bsSize='lg'
|
|
||||||
className='text-center'
|
|
||||||
keyboard={true}
|
|
||||||
onHide={onHide}
|
|
||||||
show={show}
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title id='modal-title'>
|
|
||||||
{t('settings.danger.reset-heading')}
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>{t('settings.danger.reset-p1')}</p>
|
<p>{t('settings.danger.reset-p1')}</p>
|
||||||
<p>{t('settings.danger.reset-p2')}</p>
|
<p>{t('settings.danger.reset-p2')}</p>
|
||||||
<hr />
|
</Modal.Body>
|
||||||
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
block={true}
|
block={true}
|
||||||
size='large'
|
size='large'
|
||||||
@@ -53,9 +43,6 @@ function ResetModal(props: ResetModalProps): JSX.Element {
|
|||||||
>
|
>
|
||||||
{t('settings.danger.reset-confirm')}
|
{t('settings.danger.reset-confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</Modal.Body>
|
|
||||||
<Modal.Footer>
|
|
||||||
<Button onClick={props.onHide}>{t('buttons.close')}</Button>
|
|
||||||
</Modal.Footer>
|
</Modal.Footer>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
import { bindActionCreators, Dispatch, AnyAction } from 'redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@freecodecamp/ui';
|
import { Button, Modal } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import { Spacer } from '../helpers';
|
import { Spacer } from '../helpers';
|
||||||
import { hardGoTo as navigate, closeSignoutModal } from '../../redux/actions';
|
import { hardGoTo as navigate, closeSignoutModal } from '../../redux/actions';
|
||||||
@@ -47,20 +46,9 @@ function SignoutModal(props: SignoutModalProps): JSX.Element {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal size='large' variant='danger' open={show} onClose={handleModalHide}>
|
||||||
aria-labelledby='modal-title'
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
backdrop={true}
|
{t('signout.heading')}
|
||||||
bsSize='lg'
|
|
||||||
className='text-center'
|
|
||||||
keyboard={true}
|
|
||||||
onHide={handleModalHide}
|
|
||||||
onClose={handleModalHide}
|
|
||||||
show={show}
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title id='modal-title' bsSize='lg'>
|
|
||||||
<span style={{ fontWeight: 'bold' }}>{t('signout.heading')}</span>
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from 'react';
|
||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import { Trans, useTranslation } from 'react-i18next';
|
import { Trans, useTranslation } from 'react-i18next';
|
||||||
import { Button } from '@freecodecamp/ui';
|
import { Button, Modal } from '@freecodecamp/ui';
|
||||||
|
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import { Spacer } from '../helpers';
|
import { Spacer } from '../helpers';
|
||||||
@@ -17,23 +16,11 @@ function StagingWarningModal(): JSX.Element {
|
|||||||
setShow(false);
|
setShow(false);
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal onClose={handleModalHide} open={show} size='large'>
|
||||||
aria-labelledby='modal-title'
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
backdrop={true}
|
<span style={{ fontWeight: 'bold' }}>
|
||||||
bsSize='lg'
|
{t('staging-warning.heading')}
|
||||||
className='text-center'
|
</span>
|
||||||
keyboard={true}
|
|
||||||
onHide={handleModalHide}
|
|
||||||
onClose={handleModalHide}
|
|
||||||
show={show}
|
|
||||||
data-testid={'staging-warning-modal'}
|
|
||||||
>
|
|
||||||
<Modal.Header closeButton={true}>
|
|
||||||
<Modal.Title id='modal-title' bsSize='lg'>
|
|
||||||
<span style={{ fontWeight: 'bold' }}>
|
|
||||||
{t('staging-warning.heading')}
|
|
||||||
</span>
|
|
||||||
</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body>
|
<Modal.Body>
|
||||||
<p className='text-justify'>{t('staging-warning.p1')}</p>
|
<p className='text-justify'>{t('staging-warning.p1')}</p>
|
||||||
|
|||||||
@@ -2,23 +2,40 @@ import React from 'react';
|
|||||||
import { render, screen, fireEvent } from '@testing-library/react';
|
import { render, screen, fireEvent } from '@testing-library/react';
|
||||||
import store from 'store';
|
import store from 'store';
|
||||||
import StagingWarningModal from '.';
|
import StagingWarningModal from '.';
|
||||||
|
|
||||||
describe('StagingWarningModal', () => {
|
describe('StagingWarningModal', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
// The Modal component uses `ResizeObserver` under the hood.
|
||||||
|
// However, this property is not available in JSDOM, so we need to manually add it to the window object.
|
||||||
|
// Ref: https://github.com/jsdom/jsdom/issues/3368
|
||||||
|
Object.defineProperty(window, 'ResizeObserver', {
|
||||||
|
writable: true,
|
||||||
|
value: jest.fn(() => ({
|
||||||
|
observe: jest.fn(),
|
||||||
|
unobserve: jest.fn(),
|
||||||
|
disconnect: jest.fn()
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders the modal successfully', () => {
|
it('renders the modal successfully', () => {
|
||||||
render(<StagingWarningModal />);
|
render(<StagingWarningModal />);
|
||||||
expect(screen.getByTestId('staging-warning-modal')).toBeInTheDocument();
|
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||||
expect(screen.getByTestId('staging-warning-modal')).toHaveClass('in');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes the modal when clicking the close button', () => {
|
it('closes the modal when clicking the close button', () => {
|
||||||
render(<StagingWarningModal />);
|
render(<StagingWarningModal />);
|
||||||
fireEvent.click(screen.getByText('Close'));
|
fireEvent.click(screen.getByText('Close'));
|
||||||
expect(screen.getByTestId('staging-warning-modal')).not.toHaveClass('in');
|
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('displays the correct modal content', () => {
|
it('displays the correct modal content', () => {
|
||||||
render(<StagingWarningModal />);
|
render(<StagingWarningModal />);
|
||||||
const modalContent = screen.getByTestId('staging-warning-modal');
|
const modalContent = screen.getByRole('dialog');
|
||||||
expect(modalContent).toHaveTextContent('staging-warning.heading');
|
expect(modalContent).toHaveTextContent('staging-warning.heading');
|
||||||
expect(modalContent).toHaveTextContent('staging-warning.p1');
|
expect(modalContent).toHaveTextContent('staging-warning.p1');
|
||||||
expect(modalContent).toHaveTextContent('staging-warning.p2');
|
expect(modalContent).toHaveTextContent('staging-warning.p2');
|
||||||
@@ -27,8 +44,10 @@ describe('StagingWarningModal', () => {
|
|||||||
|
|
||||||
it('accepts Warning, stores acceptance key in local storage, and closes the modal', () => {
|
it('accepts Warning, stores acceptance key in local storage, and closes the modal', () => {
|
||||||
render(<StagingWarningModal />);
|
render(<StagingWarningModal />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId('accepts-warning'));
|
fireEvent.click(screen.getByTestId('accepts-warning'));
|
||||||
expect(store.get('acceptedStagingWarning')).toBe(true);
|
expect(store.get('acceptedStagingWarning')).toBe(true);
|
||||||
expect(screen.queryByTestId('staging-warning-modal')).not.toHaveClass('in');
|
|
||||||
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,24 +0,0 @@
|
|||||||
.reset-modal p {
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-modal-header {
|
|
||||||
color: var(--danger-color);
|
|
||||||
background-color: var(--danger-background);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-modal-header h4 {
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.reset-modal-header .close {
|
|
||||||
color: var(--danger-color);
|
|
||||||
font-size: 28px;
|
|
||||||
text-shadow: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 767px) {
|
|
||||||
.reset-modal .btn-lg {
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +1,20 @@
|
|||||||
// Package Utilities
|
|
||||||
import { Modal } from '@freecodecamp/react-bootstrap';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { bindActionCreators, Dispatch } from 'redux';
|
import { bindActionCreators, Dispatch } from 'redux';
|
||||||
import { createSelector } from 'reselect';
|
import { createSelector } from 'reselect';
|
||||||
import { Button } from '@freecodecamp/ui';
|
import { Button, Modal } from '@freecodecamp/ui';
|
||||||
|
|
||||||
// Local Utilities
|
|
||||||
import { closeModal, resetChallenge } from '../redux/actions';
|
import { closeModal, resetChallenge } from '../redux/actions';
|
||||||
import { isResetModalOpenSelector } from '../redux/selectors';
|
import { isResetModalOpenSelector } from '../redux/selectors';
|
||||||
import callGA from '../../../analytics/call-ga';
|
import callGA from '../../../analytics/call-ga';
|
||||||
|
|
||||||
// Styles
|
|
||||||
import './reset-modal.css';
|
|
||||||
|
|
||||||
// Types
|
|
||||||
interface ResetModalProps {
|
interface ResetModalProps {
|
||||||
close: () => void;
|
close: () => void;
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Redux Setup
|
|
||||||
const mapStateToProps = createSelector(
|
const mapStateToProps = createSelector(
|
||||||
isResetModalOpenSelector,
|
isResetModalOpenSelector,
|
||||||
(isOpen: boolean) => ({
|
(isOpen: boolean) => ({
|
||||||
@@ -43,25 +35,17 @@ function withActions(...fns: Array<() => void>) {
|
|||||||
return () => fns.forEach(fn => fn());
|
return () => fns.forEach(fn => fn());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component
|
|
||||||
function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
|
function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
callGA({ event: 'pageview', pagePath: '/reset-modal' });
|
callGA({ event: 'pageview', pagePath: '/reset-modal' });
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal onClose={close} open={isOpen} variant='danger'>
|
||||||
data-playwright-test-label='reset-modal'
|
<Modal.Header showCloseButton={true} closeButtonClassNames='close'>
|
||||||
animation={false}
|
{t('learn.reset')}
|
||||||
dialogClassName='reset-modal'
|
|
||||||
keyboard={true}
|
|
||||||
onHide={close}
|
|
||||||
show={isOpen}
|
|
||||||
>
|
|
||||||
<Modal.Header className='reset-modal-header' closeButton={true}>
|
|
||||||
<Modal.Title className='text-center'>{t('learn.reset')}</Modal.Title>
|
|
||||||
</Modal.Header>
|
</Modal.Header>
|
||||||
<Modal.Body className='reset-modal-body'>
|
<Modal.Body>
|
||||||
<div className='text-center'>
|
<div className='text-center'>
|
||||||
<p>{t('learn.reset-warn')}</p>
|
<p>{t('learn.reset-warn')}</p>
|
||||||
<p>
|
<p>
|
||||||
@@ -69,7 +53,7 @@ function ResetModal({ reset, close, isOpen }: ResetModalProps): JSX.Element {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Modal.Body>
|
</Modal.Body>
|
||||||
<Modal.Footer className='reset-modal-footer'>
|
<Modal.Footer>
|
||||||
<Button
|
<Button
|
||||||
data-cy='reset-modal-confirm'
|
data-cy='reset-modal-confirm'
|
||||||
block={true}
|
block={true}
|
||||||
|
|||||||
+26
-22
@@ -13,20 +13,16 @@ test.describe('Delete Modal component', () => {
|
|||||||
.getByRole('button', { name: translations.settings.danger.delete })
|
.getByRole('button', { name: translations.settings.danger.delete })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
// There are two elements with the `dialog` role in the DOM.
|
|
||||||
// This appears to be semantically incorrect and should be resolved
|
|
||||||
// once we have migrated the component to use Dialog from the `ui-components` library.
|
|
||||||
const dialogs = await page.getByRole('dialog').all();
|
|
||||||
expect(dialogs).toHaveLength(2);
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('heading', {
|
page.getByRole('dialog', {
|
||||||
name: translations.settings.danger['delete-title']
|
name: translations.settings.danger['delete-title']
|
||||||
})
|
})
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(translations.settings.danger['delete-p1'])
|
page.getByText(translations.settings.danger['delete-p1'])
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(translations.settings.danger['delete-p2'])
|
page.getByText(translations.settings.danger['delete-p2'])
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
@@ -47,11 +43,9 @@ test.describe('Delete Modal component', () => {
|
|||||||
page.getByRole('button', { name: translations.settings.danger.certain })
|
page.getByRole('button', { name: translations.settings.danger.certain })
|
||||||
).toBeVisible();
|
).toBeVisible();
|
||||||
|
|
||||||
// There are 2 close buttons on the modal: one is sr-only on top, and one on the bottom of modal
|
await expect(
|
||||||
const closeButtons = await page
|
page.getByRole('button', { name: translations.buttons.close })
|
||||||
.getByRole('button', { name: translations.buttons.close })
|
).toBeVisible();
|
||||||
.all();
|
|
||||||
expect(closeButtons).toHaveLength(2);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close the modal after the user cancels account deleting', async ({
|
test('should close the modal after the user cancels account deleting', async ({
|
||||||
@@ -61,16 +55,21 @@ test.describe('Delete Modal component', () => {
|
|||||||
.getByRole('button', { name: translations.settings.danger.delete })
|
.getByRole('button', { name: translations.settings.danger.delete })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const dialogs = await page.getByRole('dialog').all();
|
await expect(
|
||||||
expect(dialogs).toHaveLength(2);
|
page.getByRole('dialog', {
|
||||||
|
name: translations.settings.danger['delete-title']
|
||||||
|
})
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: translations.settings.danger.nevermind })
|
.getByRole('button', { name: translations.settings.danger.nevermind })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
for (const dialog of dialogs) {
|
await expect(
|
||||||
await expect(dialog).not.toBeVisible();
|
page.getByRole('dialog', {
|
||||||
}
|
name: translations.settings.danger['delete-title']
|
||||||
|
})
|
||||||
|
).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should close the modal and redirect to /learn after the user clicks delete', async ({
|
test('should close the modal and redirect to /learn after the user clicks delete', async ({
|
||||||
@@ -87,16 +86,21 @@ test.describe('Delete Modal component', () => {
|
|||||||
.getByRole('button', { name: translations.settings.danger.delete })
|
.getByRole('button', { name: translations.settings.danger.delete })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const dialogs = await page.getByRole('dialog').all();
|
await expect(
|
||||||
expect(dialogs).toHaveLength(2);
|
page.getByRole('dialog', {
|
||||||
|
name: translations.settings.danger['delete-title']
|
||||||
|
})
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: translations.settings.danger.certain })
|
.getByRole('button', { name: translations.settings.danger.certain })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
for (const dialog of dialogs) {
|
await expect(
|
||||||
await expect(dialog).not.toBeVisible();
|
page.getByRole('dialog', {
|
||||||
}
|
name: translations.settings.danger['delete-title']
|
||||||
|
})
|
||||||
|
).not.toBeVisible();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/.*\/learn\/?/);
|
await expect(page).toHaveURL(/.*\/learn\/?/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -33,9 +33,9 @@ test('Click on the "Reset" button', async ({ page }) => {
|
|||||||
const resetButton = page.getByTestId('lowerJaw-reset-button');
|
const resetButton = page.getByTestId('lowerJaw-reset-button');
|
||||||
await resetButton.click();
|
await resetButton.click();
|
||||||
|
|
||||||
const resetModal = page.getByTestId('reset-modal');
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: 'Reset this lesson?' })
|
||||||
await expect(resetModal).toBeVisible();
|
).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should render UI correctly', async ({ page }) => {
|
test('Should render UI correctly', async ({ page }) => {
|
||||||
|
|||||||
+9
-11
@@ -16,11 +16,9 @@ test('should render the modal content correctly', async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: translations.buttons.reset }).click();
|
await page.getByRole('button', { name: translations.buttons.reset }).click();
|
||||||
|
|
||||||
// There are two elements with the `dialog` role in the DOM.
|
await expect(
|
||||||
// This appears to be semantically incorrect and should be resolved
|
page.getByRole('dialog', { name: translations.learn.reset })
|
||||||
// once we have migrated the component to use Dialog from the `ui-components` library.
|
).toBeVisible();
|
||||||
const dialogs = await page.getByRole('dialog').all();
|
|
||||||
expect(dialogs).toHaveLength(2);
|
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByRole('button', {
|
page.getByRole('button', {
|
||||||
@@ -138,11 +136,9 @@ test('should close when the user clicks the close button', async ({ page }) => {
|
|||||||
|
|
||||||
await page.getByRole('button', { name: translations.buttons.reset }).click();
|
await page.getByRole('button', { name: translations.buttons.reset }).click();
|
||||||
|
|
||||||
// There are two elements with the `dialog` role in the DOM.
|
await expect(
|
||||||
// This appears to be semantically incorrect and should be resolved
|
page.getByRole('dialog', { name: translations.learn.reset })
|
||||||
// once we have migrated the component to use Dialog from the `ui-components` library.
|
).toBeVisible();
|
||||||
const dialogs = await page.getByRole('dialog').all();
|
|
||||||
expect(dialogs).toHaveLength(2);
|
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', {
|
.getByRole('button', {
|
||||||
@@ -150,5 +146,7 @@ test('should close when the user clicks the close button', async ({ page }) => {
|
|||||||
})
|
})
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
await expect(page.getByRole('dialog')).toBeHidden();
|
await expect(
|
||||||
|
page.getByRole('dialog', { name: translations.learn.reset })
|
||||||
|
).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|||||||
+15
-15
@@ -14,10 +14,10 @@ test.describe('Signout Modal component', () => {
|
|||||||
.getByRole('button', { name: translations.buttons['sign-out'] })
|
.getByRole('button', { name: translations.buttons['sign-out'] })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const dialogs = page.getByRole('dialog');
|
await expect(
|
||||||
await expect(dialogs).toHaveCount(2);
|
page.getByRole('dialog', { name: translations.signout.heading })
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByText(translations.signout.heading)).toBeVisible();
|
|
||||||
await expect(page.getByText(translations.signout.p1)).toBeVisible();
|
await expect(page.getByText(translations.signout.p1)).toBeVisible();
|
||||||
await expect(page.getByText(translations.signout.p2)).toBeVisible();
|
await expect(page.getByText(translations.signout.p2)).toBeVisible();
|
||||||
|
|
||||||
@@ -40,16 +40,17 @@ test.describe('Signout Modal component', () => {
|
|||||||
.getByRole('button', { name: translations.buttons['sign-out'] })
|
.getByRole('button', { name: translations.buttons['sign-out'] })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const dialogs = page.getByRole('dialog');
|
await expect(
|
||||||
await expect(dialogs).toHaveCount(2);
|
page.getByRole('dialog', { name: translations.signout.heading })
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: translations.signout.certain })
|
.getByRole('button', { name: translations.signout.certain })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
for (const dialog of await dialogs.all()) {
|
await expect(
|
||||||
await expect(dialog).not.toBeVisible();
|
page.getByRole('dialog', { name: translations.signout.heading })
|
||||||
}
|
).not.toBeVisible();
|
||||||
await expect(page).toHaveURL(/.*\/learn\/?$/);
|
await expect(page).toHaveURL(/.*\/learn\/?$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -59,19 +60,18 @@ test.describe('Signout Modal component', () => {
|
|||||||
.getByRole('button', { name: translations.buttons['sign-out'] })
|
.getByRole('button', { name: translations.buttons['sign-out'] })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
const dialogs = page.getByRole('dialog');
|
await expect(
|
||||||
await expect(dialogs).toHaveCount(2);
|
page.getByRole('dialog', { name: translations.signout.heading })
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByRole('button', { name: translations.signout.nevermind })
|
.getByRole('button', { name: translations.signout.nevermind })
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
for (const dialog of await dialogs.all()) {
|
|
||||||
await expect(dialog).not.toBeVisible();
|
|
||||||
}
|
|
||||||
await expect(page).toHaveURL('/');
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByText(translations.signout.heading)
|
page.getByRole('dialog', { name: translations.signout.heading })
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
|
await expect(page).toHaveURL('/');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import { test, expect } from '@playwright/test';
|
|
||||||
|
|
||||||
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto('/settings');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Multifile dropdown testing', async ({ page }) => {
|
|
||||||
const multifile_dropdown = page.getByTestId('multifile-dropdown');
|
|
||||||
const isVisible = await multifile_dropdown.isVisible();
|
|
||||||
if (isVisible) {
|
|
||||||
await multifile_dropdown.click();
|
|
||||||
const multifile_dropdown_code = page.getByTestId('multifile-dropdown-code');
|
|
||||||
const multifile_dropdown_project = page.getByTestId(
|
|
||||||
'multifile-dropdown-project'
|
|
||||||
);
|
|
||||||
await expect(multifile_dropdown_code).toBeVisible();
|
|
||||||
await expect(multifile_dropdown_project).toBeVisible();
|
|
||||||
|
|
||||||
await multifile_dropdown_code.click();
|
|
||||||
const project_solution_viewer_modal = page.getByTestId(
|
|
||||||
'project-solution-viewer-modal'
|
|
||||||
);
|
|
||||||
await expect(project_solution_viewer_modal).toBeVisible();
|
|
||||||
const close_button = page.locator('button.close');
|
|
||||||
await close_button.click();
|
|
||||||
|
|
||||||
await multifile_dropdown.click();
|
|
||||||
await multifile_dropdown_project.click();
|
|
||||||
const project_preview_modal = page.getByTestId('project-preview-modal');
|
|
||||||
await expect(project_preview_modal).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('Single Solution testing', async ({ page }) => {
|
|
||||||
const solution_button = page.getByRole('button', {
|
|
||||||
name: /Solution for Palindrome Checker/i
|
|
||||||
});
|
|
||||||
const isVisible = await solution_button.isVisible();
|
|
||||||
if (isVisible) {
|
|
||||||
await solution_button.click();
|
|
||||||
const solution_viewer_modal = page.getByTestId(
|
|
||||||
'project-solution-viewer-modal'
|
|
||||||
);
|
|
||||||
await expect(solution_viewer_modal).toBeVisible();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('External solution testing', async ({ page }) => {
|
|
||||||
const solution_button = page
|
|
||||||
.getByRole('button', {
|
|
||||||
name: /Solution for Build a Random Quote Machine/i
|
|
||||||
})
|
|
||||||
.first();
|
|
||||||
const isVisible = await solution_button.isVisible();
|
|
||||||
if (isVisible) {
|
|
||||||
const browserContext = page.context();
|
|
||||||
|
|
||||||
const [newPage] = await Promise.all([
|
|
||||||
browserContext.waitForEvent('page'),
|
|
||||||
solution_button.click()
|
|
||||||
]);
|
|
||||||
|
|
||||||
await newPage.waitForLoadState();
|
|
||||||
|
|
||||||
await expect(newPage).toHaveURL(/^https:\/\/codepen\.io/);
|
|
||||||
|
|
||||||
await newPage.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
+51
-14
@@ -2,25 +2,19 @@ import { test, expect } from '@playwright/test';
|
|||||||
|
|
||||||
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
test.use({ storageState: 'playwright/.auth/certified-user.json' });
|
||||||
|
|
||||||
test.beforeEach(async ({ page }) => {
|
|
||||||
await page.goto(
|
|
||||||
'/certification/certifieduser/javascript-algorithms-and-data-structures'
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe('Solution Viewer component', () => {
|
test.describe('Solution Viewer component', () => {
|
||||||
test('renders the modal correctly', async ({ page }) => {
|
test('renders the modal correctly', async ({ page }) => {
|
||||||
await page.getByRole('button').filter({ hasText: /view/i }).first().click();
|
await page.goto(
|
||||||
|
'/certification/certifieduser/javascript-algorithms-and-data-structures'
|
||||||
const projectSolutionViewerModal = page.getByTestId(
|
|
||||||
'project-solution-viewer-modal'
|
|
||||||
);
|
);
|
||||||
await expect(projectSolutionViewerModal).toBeVisible();
|
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: /view/i }).first().click();
|
||||||
|
|
||||||
|
const projectSolutionViewerModal = page.getByRole('dialog', {
|
||||||
|
name: 'Solution for'
|
||||||
|
});
|
||||||
// The modal should show the solution title...
|
// The modal should show the solution title...
|
||||||
await expect(
|
await expect(projectSolutionViewerModal).toBeVisible();
|
||||||
page.getByRole('heading').and(page.getByText(/solution for/i))
|
|
||||||
).toBeVisible();
|
|
||||||
// ...and relevant code file/s
|
// ...and relevant code file/s
|
||||||
await expect(page.getByText(/js/i)).toBeVisible();
|
await expect(page.getByText(/js/i)).toBeVisible();
|
||||||
await expect(page.locator('pre').first()).toBeVisible();
|
await expect(page.locator('pre').first()).toBeVisible();
|
||||||
@@ -38,4 +32,47 @@ test.describe('Solution Viewer component', () => {
|
|||||||
await bottomRightCloseButton.click();
|
await bottomRightCloseButton.click();
|
||||||
await expect(projectSolutionViewerModal).toBeHidden();
|
await expect(projectSolutionViewerModal).toBeHidden();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('renders external project links correctly', async ({ page }) => {
|
||||||
|
await page.goto(
|
||||||
|
'/certification/certifieduser/front-end-development-libraries'
|
||||||
|
);
|
||||||
|
|
||||||
|
const projectLink = page.getByRole('link', { name: 'View' }).first();
|
||||||
|
const browserContext = page.context();
|
||||||
|
|
||||||
|
const [newPage] = await Promise.all([
|
||||||
|
browserContext.waitForEvent('page'),
|
||||||
|
projectLink.click()
|
||||||
|
]);
|
||||||
|
|
||||||
|
await newPage.waitForLoadState();
|
||||||
|
|
||||||
|
await expect(newPage).toHaveURL(/^https:\/\/codepen\.io/);
|
||||||
|
|
||||||
|
await newPage.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('render projects with multiple solutions correctly', async ({
|
||||||
|
page
|
||||||
|
}) => {
|
||||||
|
await page.goto('/certification/certifieduser/quality-assurance-v7');
|
||||||
|
|
||||||
|
const dropdownButton = page.getByRole('button', { name: 'View' }).first();
|
||||||
|
await dropdownButton.click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('menu')).toBeVisible();
|
||||||
|
|
||||||
|
const sourceLink = page.getByRole('menuitem', { name: /source/i }).first();
|
||||||
|
|
||||||
|
const browserContext = page.context();
|
||||||
|
const [newPage] = await Promise.all([
|
||||||
|
browserContext.waitForEvent('page'),
|
||||||
|
sourceLink.click()
|
||||||
|
]);
|
||||||
|
|
||||||
|
await newPage.waitForLoadState();
|
||||||
|
|
||||||
|
await newPage.close();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
import React, {
|
import React, { createContext, useContext, Fragment } from 'react';
|
||||||
type ReactNode,
|
|
||||||
createContext,
|
|
||||||
useContext,
|
|
||||||
Fragment
|
|
||||||
} from 'react';
|
|
||||||
import { Dialog, Transition } from '@headlessui/react';
|
import { Dialog, Transition } from '@headlessui/react';
|
||||||
|
|
||||||
import { CloseButton } from '../close-button';
|
import { CloseButton } from '../close-button';
|
||||||
import { type ModalProps, type HeaderProps, BodyProps } from './types';
|
import type { ModalProps, HeaderProps, BodyProps, FooterProps } from './types';
|
||||||
|
|
||||||
// There is a close button on the right side of the modal title.
|
// There is a close button on the right side of the modal title.
|
||||||
// Some extra padding needs to be added to the left of the title text
|
// Some extra padding needs to be added to the left of the title text
|
||||||
@@ -73,8 +68,14 @@ const Body = ({ children, alignment = 'center' }: BodyProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Footer = ({ children }: { children: ReactNode }) => {
|
const Footer = ({ children, alignment = 'center' }: FooterProps) => {
|
||||||
return <div className='p-[15px]'>{children}</div>;
|
if (alignment === 'end') {
|
||||||
|
return <div className='p-[15px] flex justify-end'>{children}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`p-[15px] flex flex-col justify-center`}>{children}</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Modal = ({
|
const Modal = ({
|
||||||
@@ -101,12 +102,12 @@ const Modal = ({
|
|||||||
return (
|
return (
|
||||||
<ModalContext.Provider value={{ onClose, variant }}>
|
<ModalContext.Provider value={{ onClose, variant }}>
|
||||||
<Transition.Root show={open} as={Fragment}>
|
<Transition.Root show={open} as={Fragment}>
|
||||||
<Dialog onClose={onClose} className='relative z-1050'>
|
<Dialog onClose={onClose} className='relative z-1050 w-screen h-screen'>
|
||||||
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
{/* The backdrop, rendered as a fixed sibling to the panel container */}
|
||||||
<div aria-hidden className='fixed inset-0 bg-gray-900 opacity-50' />
|
<div aria-hidden className='fixed inset-0 bg-gray-900 opacity-50' />
|
||||||
|
|
||||||
{/* Full-screen container of the panel */}
|
{/* Full-screen container of the panel */}
|
||||||
<div className='fixed inset-0 w-screen flex items-start justify-center pt-[30px]'>
|
<div className='fixed inset-0 flex items-start justify-center pt-[30px] pb-[30px] overflow-scroll'>
|
||||||
<Transition.Child
|
<Transition.Child
|
||||||
as={Fragment}
|
as={Fragment}
|
||||||
enter='transition-all duration-300 ease-out'
|
enter='transition-all duration-300 ease-out'
|
||||||
|
|||||||
@@ -32,3 +32,8 @@ export interface BodyProps {
|
|||||||
*/
|
*/
|
||||||
alignment?: 'center' | 'left' | 'start';
|
alignment?: 'center' | 'left' | 'start';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FooterProps {
|
||||||
|
children: ReactNode;
|
||||||
|
alignment?: 'center' | 'end';
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user