Create Preview Loading screen (#17914)

* Minor changes

* Basic implementation with some bugs

* Removed files

* Correct tab selection

* Added tests

* Disabled bottom bar buttons on preview loading

* Applied other changes

* Added tests for PageTitle

* Added tests for LoadingPreview

* Added test for Options.jsx

* Removed comments

* Fixed CSS

* Removed unused code

* Fixed test failure

* Added aria-live

* Nit fixe

* Fixed tests

* Applied suggested changes
This commit is contained in:
Rajat Talesra 2022-07-07 19:21:50 +05:30 committed by GitHub
parent 390cf0a6a0
commit 5966960c04
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 321 additions and 10 deletions

View file

@ -264,9 +264,14 @@
'width',
(
'0': 0,
'10': 10%,
'15': 15%,
'25': 25%,
'50': 50%,
'60': 60%,
'70': 70%,
'75': 75%,
'80': 80%,
'100': 100%,
'auto': auto,
'full': 100vw,

View file

@ -257,3 +257,20 @@
}
}
}
.crayons-preview {
// Image cover section for preview loading.
&__cover {
display: block;
width: 100%;
height: auto;
padding-bottom: 42%;
background-size: cover;
background-color: var(--body-bg);
background-position: center center;
@media (min-width: $breakpoint-s) {
border-radius: var(--radius) var(--radius) 0 0;
}
}
}

View file

@ -107,7 +107,8 @@ export class ArticleForm extends Component {
bodyMarkdown: this.article.body_markdown || '',
published: this.article.published || false,
previewShowing: false,
previewResponse: '',
previewLoading: false,
previewResponse: { processed_html: '' },
submitting: false,
editing: this.article.id !== null, // eslint-disable-line react/no-unused-state
mainImage: this.article.main_image || null,
@ -137,7 +138,7 @@ export class ArticleForm extends Component {
componentDidUpdate() {
const { previewResponse } = this.state;
if (previewResponse) {
if (previewResponse?.processed_html) {
embedGists(this.formElement);
this.constructor.handleRunkitPreview();
this.constructor.handleAsciinemaPreview();
@ -161,11 +162,13 @@ export class ArticleForm extends Component {
setCommonProps = ({
previewShowing = false,
previewLoading = false,
helpFor = null,
helpPosition = null,
}) => {
return {
previewShowing,
previewLoading,
helpFor,
helpPosition,
};
@ -179,6 +182,7 @@ export class ArticleForm extends Component {
...this.setCommonProps({}),
});
} else {
this.showLoadingPreview();
previewArticle(bodyMarkdown, this.showPreview, this.failedPreview);
}
};
@ -221,10 +225,22 @@ export class ArticleForm extends Component {
}
};
showLoadingPreview = () => {
this.setState({
...this.setCommonProps({
previewShowing: true,
previewLoading: true,
}),
});
};
showPreview = (response) => {
this.fetchMarkdownLint();
this.setState({
...this.setCommonProps({ previewShowing: true }),
...this.setCommonProps({
previewShowing: true,
previewLoading: false,
}),
previewResponse: response,
errors: null,
});
@ -237,6 +253,7 @@ export class ArticleForm extends Component {
failedPreview = (response) => {
this.setState({
...this.setCommonProps({ previewLoading: false }),
errors: response,
submitting: false,
});
@ -319,6 +336,7 @@ export class ArticleForm extends Component {
bodyMarkdown: this.article.body_markdown || '',
published: this.article.published || false,
previewShowing: false,
previewLoading: false,
previewResponse: '',
submitting: false,
editing: this.article.id !== null, // eslint-disable-line react/no-unused-state
@ -375,6 +393,7 @@ export class ArticleForm extends Component {
bodyMarkdown,
published,
previewShowing,
previewLoading,
previewResponse,
submitting,
organizations,
@ -403,6 +422,7 @@ export class ArticleForm extends Component {
>
<Header
onPreview={this.fetchPreview}
previewLoading={previewLoading}
previewShowing={previewShowing}
organizations={organizations}
organizationId={organizationId}
@ -411,8 +431,14 @@ export class ArticleForm extends Component {
displayModal={() => this.showModal(true)}
/>
{previewShowing ? (
<span aria-live="polite" className="screen-reader-only">
{previewLoading ? 'Loading preview' : null}
{previewShowing && !previewLoading ? 'Preview loaded' : null}
</span>
{previewShowing || previewLoading ? (
<Preview
previewLoading={previewLoading}
previewResponse={previewResponse}
articleState={this.state}
errors={errors}
@ -473,6 +499,7 @@ export class ArticleForm extends Component {
passedData={this.state}
onConfigChange={this.handleConfigChange}
submitting={submitting}
previewLoading={previewLoading}
/>
<KeyboardShortcuts

View file

@ -13,6 +13,7 @@ export const EditorActions = ({
passedData,
onConfigChange,
submitting,
previewLoading,
}) => {
const isVersion1 = version === 'v1';
const isVersion2 = version === 'v2';
@ -40,12 +41,17 @@ export const EditorActions = ({
variant="primary"
className="mr-2 whitespace-nowrap"
onClick={onPublish}
disabled={previewLoading}
>
{published || isVersion1 ? 'Save changes' : 'Publish'}
</Button>
{!(published || isVersion1) && (
<Button className="mr-2 whitespace-nowrap" onClick={onSaveDraft}>
<Button
className="mr-2 whitespace-nowrap"
onClick={onSaveDraft}
disabled={previewLoading}
>
Save <span className="hidden s:inline">draft</span>
</Button>
)}
@ -55,6 +61,7 @@ export const EditorActions = ({
passedData={passedData}
onConfigChange={onConfigChange}
onSaveDraft={onSaveDraft}
previewLoading={previewLoading}
/>
)}
@ -62,6 +69,7 @@ export const EditorActions = ({
<Button
onClick={onClearChanges}
className="whitespace-nowrap fw-normal fs-s"
disabled={previewLoading}
>
Revert <span className="hidden s:inline">new changes</span>
</Button>
@ -80,6 +88,7 @@ EditorActions.propTypes = {
passedData: PropTypes.object.isRequired,
onConfigChange: PropTypes.func.isRequired,
submitting: PropTypes.bool.isRequired,
previewLoading: PropTypes.bool.isRequired,
};
EditorActions.displayName = 'EditorActions';

View file

@ -6,6 +6,7 @@ import { PageTitle } from './PageTitle';
export const Header = ({
onPreview,
previewLoading,
previewShowing,
organizations,
organizationId,
@ -24,6 +25,7 @@ export const Header = ({
organizations={organizations}
organizationId={organizationId}
onToggle={onToggle}
previewLoading={previewLoading}
/>
<Tabs onPreview={onPreview} previewShowing={previewShowing} />
<Close displayModal={displayModal} />
@ -34,6 +36,7 @@ export const Header = ({
Header.propTypes = {
displayModal: PropTypes.func.isRequired,
onPreview: PropTypes.func.isRequired,
previewLoading: PropTypes.bool.isRequired,
previewShowing: PropTypes.bool.isRequired,
organizations: PropTypes.arrayOf(PropTypes.object).isRequired,
organizationId: PropTypes.string,

View file

@ -0,0 +1,32 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
export const LoadingPreview = ({ version }) => {
const cover = version === 'cover' && (
<div
className="crayons-preview__cover"
data-testid="loading-preview__cover"
>
<div className="crayons-scaffold" loading="lazy" />
</div>
);
return (
<div data-testid="loading-preview" title="Loading preview...">
{cover}
<div className="crayons-story__indention w-100 mt-6 ">
<div className="crayons-scaffold-loading w-50 h-0 py-4 mb-2" />
<div className="crayons-story__meta w-100 mb-5">
<div className="crayons-scaffold-loading w-10 h-0 py-3 mr-2" />
<div className="crayons-scaffold-loading w-15 h-0 py-3" />
</div>
<div className="crayons-scaffold-loading w-80 h-0 py-3 mb-2" />
<div className="crayons-scaffold-loading w-60 h-0 py-3 mb-2" />
<div className="crayons-scaffold-loading w-70 h-0 py-3 mb-2" />
</div>
</div>
);
};
LoadingPreview.propTypes = {
version: PropTypes.oneOf(['default', 'cover']),
};

View file

@ -20,6 +20,7 @@ export const Options = ({
},
onSaveDraft,
onConfigChange,
previewLoading,
}) => {
let publishedField = '';
let existingSeries = '';
@ -72,6 +73,7 @@ export const Options = ({
icon={CogIcon}
title="Post options"
aria-label="Post options"
disabled={previewLoading}
/>
<Dropdown
@ -144,6 +146,7 @@ Options.propTypes = {
}).isRequired,
onSaveDraft: PropTypes.func.isRequired,
onConfigChange: PropTypes.func.isRequired,
previewLoading: PropTypes.bool.isRequired,
};
Options.displayName = 'Options';

View file

@ -2,11 +2,16 @@ import { h } from 'preact';
import PropTypes from 'prop-types';
import { OrganizationPicker } from '../../organization/OrganizationPicker';
export const PageTitle = ({ organizations, organizationId, onToggle }) => {
export const PageTitle = ({
organizations,
organizationId,
onToggle,
previewLoading,
}) => {
return (
<div className="crayons-field__label flex items-center flex-1">
<span className="hidden s:inline-block mr-2 whitespace-nowrap">
Create Post
{previewLoading ? 'Loading preview' : 'Create Post'}
</span>
{organizations && organizations.length > 0 && (
<div>
@ -29,6 +34,7 @@ PageTitle.propTypes = {
organizations: PropTypes.arrayOf(PropTypes.string).isRequired,
organizationId: PropTypes.string,
onToggle: PropTypes.func.isRequired,
previewLoading: PropTypes.bool.isRequired,
};
PageTitle.displayName = 'Organization';

View file

@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { useEffect } from 'preact/hooks';
import { ErrorList } from './ErrorList';
import { AccessibilitySuggestions } from './AccessibilitySuggestions';
import { LoadingPreview } from './LoadingPreview';
function titleArea({
previewResponse,
@ -80,17 +81,30 @@ const previewResponsePropTypes = PropTypes.shape({
});
export const Preview = ({
previewLoading,
previewResponse,
articleState,
errors,
markdownLintErrors,
}) => {
useEffect(() => {
if (previewResponse.processed_html.includes('twitter-timeline')) {
if (previewResponse?.processed_html?.includes('twitter-timeline')) {
attachTwitterTimelineScript();
}
}, [previewResponse]);
if (previewLoading) {
const coverImage = articleState.mainImage;
const loadingPreview = (
<LoadingPreview version={coverImage === null ? 'default' : 'cover'} />
);
return (
<div className="crayons-article-form__content crayons-card">
{loadingPreview}
</div>
);
}
return (
<div className="crayons-article-form__content crayons-card">
<article className="crayons-article">
@ -123,6 +137,7 @@ function attachTwitterTimelineScript() {
}
Preview.propTypes = {
previewLoading: PropTypes.bool,
previewResponse: previewResponsePropTypes.isRequired,
errors: PropTypes.object,
markdownLintErrors: PropTypes.arrayOf(PropTypes.object),

View file

@ -0,0 +1,25 @@
import { h } from 'preact';
import { render } from '@testing-library/preact';
import { axe } from 'jest-axe';
import { LoadingPreview } from '..';
describe('<LoadingPreview />', () => {
it('should have no a11y violations', async () => {
const { container } = render(<LoadingPreview />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('should render', () => {
const { queryByTitle } = render(<LoadingPreview />);
expect(queryByTitle('Loading preview...')).toBeDefined();
});
it('should render with cover image', () => {
const { queryByTitle } = render(<LoadingPreview version="cover" />);
expect(queryByTitle('Loading preview...')).toBeDefined();
});
});

View file

@ -50,6 +50,7 @@ describe('<Options />', () => {
onSaveDraft={null}
moreConfigShowing={null}
toggleMoreConfig={null}
previewLoading={false}
/>,
);
const results = await axe(container);
@ -57,6 +58,58 @@ describe('<Options />', () => {
expect(results).toHaveNoViolations();
});
it('should have no a11y violations when preview is loading', async () => {
const { container } = render(
<Options
passedData={getPassedData()}
onConfigChange={null}
onSaveDraft={null}
moreConfigShowing={null}
toggleMoreConfig={null}
previewLoading={true}
/>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('shows the button is disabled when preview is loading', () => {
const passedData = getPassedData();
passedData.published = true;
const { getByTitle } = render(
<Options
passedData={passedData}
onConfigChange={null}
onSaveDraft={null}
moreConfigShowing={null}
toggleMoreConfig={null}
previewLoading={true}
/>,
);
expect(getByTitle('Post options')).toBeDisabled();
});
it('shows the button is enabled when preview is not loading', () => {
const passedData = getPassedData();
passedData.published = true;
const { getByTitle } = render(
<Options
passedData={passedData}
onConfigChange={null}
onSaveDraft={null}
moreConfigShowing={null}
toggleMoreConfig={null}
previewLoading={false}
/>,
);
expect(getByTitle('Post options')).not.toBeDisabled();
});
it('shows the danger zone once an article is published', () => {
const passedData = getPassedData();
passedData.published = true;

View file

@ -29,6 +29,7 @@ describe('<PageTitle/>', () => {
organizations={organizations}
organizationId={organizationId}
onToggle={onToggle}
previewLoading={false}
/>,
);
const results = await axe(container);
@ -36,12 +37,53 @@ describe('<PageTitle/>', () => {
expect(results).toHaveNoViolations();
});
it('should have no a11y violations when preview is loading', async () => {
const { container } = render(
<PageTitle
organizations={organizations}
organizationId={organizationId}
onToggle={onToggle}
previewLoading={true}
/>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('show create post label when preview is not loading', async () => {
const { getByText } = render(
<PageTitle
organizations={organizations}
organizationId={organizationId}
onToggle={onToggle}
previewLoading={false}
/>,
);
expect(getByText(`Create Post`)).toBeInTheDocument();
});
it('show loading preview label when preview is loading', async () => {
const { getByText } = render(
<PageTitle
organizations={organizations}
organizationId={organizationId}
onToggle={onToggle}
previewLoading={true}
/>,
);
expect(getByText(`Loading preview`)).toBeInTheDocument();
});
it('shows the picker if there is more than one organisation', () => {
const { getByRole } = render(
<PageTitle
organizations={organizations}
organizationId={organizationId}
onToggle={onToggle}
previewLoading={false}
/>,
);
@ -56,6 +98,7 @@ describe('<PageTitle/>', () => {
organizations={[]}
organizationId={organizationId}
onToggle={onToggle}
previewLoading={false}
/>,
);

View file

@ -67,9 +67,10 @@ describe('<Preview />', () => {
errors = null;
});
it('should have no a11y violations', async () => {
it('should have no a11y violations when preview is loading', async () => {
const { container } = render(
<Preview
previewLoading={false}
previewResponse={getPreviewResponse()}
articleState={getArticleState()}
errors={errors}
@ -80,6 +81,78 @@ describe('<Preview />', () => {
expect(results).toHaveNoViolations();
});
it('should have no a11y violations when preview is displayed', async () => {
const { container } = render(
<Preview
previewLoading={true}
previewResponse={getPreviewResponse()}
articleState={getArticleState()}
errors={errors}
/>,
);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
it('does not show loading screen when preview is displayed', () => {
const articleState = { ...getArticleState(), mainImage: null };
const previewResponse = { ...getPreviewResponse(), cover_image: null };
const { queryByTestId } = render(
<Preview
previewLoading={false}
previewResponse={previewResponse}
articleState={articleState}
errors={errors}
/>,
);
expect(queryByTestId('loading-preview')).not.toBeInTheDocument();
});
it('shows loading screen when preview is loading', () => {
const articleState = { ...getArticleState(), mainImage: null };
const previewResponse = { ...getPreviewResponse(), cover_image: null };
const { queryByTestId } = render(
<Preview
previewLoading={true}
previewResponse={previewResponse}
articleState={articleState}
errors={errors}
/>,
);
expect(queryByTestId('loading-preview')).toBeInTheDocument();
});
it('shows loading with cover image when image is attached', () => {
const { queryByTestId } = render(
<Preview
previewLoading={true}
previewResponse={getPreviewResponse()}
articleState={getArticleState()}
errors={errors}
/>,
);
expect(queryByTestId('loading-preview__cover')).toBeInTheDocument();
});
it('shows preview loading without cover image loading', () => {
const articleState = { ...getArticleState(), mainImage: null };
const previewResponse = { ...getPreviewResponse(), cover_image: null };
const { queryByTestId } = render(
<Preview
previewLoading={true}
previewResponse={previewResponse}
articleState={articleState}
errors={errors}
/>,
);
expect(queryByTestId('loading-preview__cover')).not.toBeInTheDocument();
});
it('shows the correct title', () => {
const previewResponse = getPreviewResponse();
const { getByText } = render(
@ -133,7 +206,7 @@ describe('<Preview />', () => {
/>,
);
expect(queryByTestId('article-form__cover')).not.toBeInTheDocument();
expect(queryByTestId('page-title__label')).not.toBeInTheDocument();
});
// TODO: need to write a test for the cover image for v1