Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions __TESTS__/css.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/* eslint-disable import/no-extraneous-dependencies */
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
/* eslint-enable import/no-extraneous-dependencies */
import React from 'react';

import Css from '../pages/css';

describe('CSS', () => {
it('formats CSS correctly', async () => {
const user = userEvent.setup();
render(<Css />);

const css = screen.getByLabelText(/^CSS$/i);
const formattedCss = screen.getByLabelText(/FormattedCSS/i);

await user.clear(css);
await user.type(
css,
'body{background:#ffffff;}'.replace(/[{[]/g, '$&$&'),
);
expect(formattedCss.innerHTML).toBe(`body {
background: #ffffff;
}
`);
});

it('minifies CSS correctly', async () => {
const user = userEvent.setup();
render(<Css />);

const css = screen.getByLabelText(/^CSS$/i);
const minifySwitch = screen.getByRole('checkbox', {
name: /Minify/i,
});

await user.clear(css);
await user.type(
css,
`body{
background:#ffffff;
}`.replace(/[{[]/g, '$&$&'),
);

await user.click(minifySwitch);
const minifiedCss = screen.getByLabelText(/MinifiedCSS/i);

expect(minifiedCss.innerHTML).toBe('body{background:#fff}');

await user.click(minifySwitch);
});

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we also test that minify works correctly?

it('clears inputs with either clear button', async () => {
const user = userEvent.setup();
render(<Css />);

const css = screen.getByLabelText(/^CSS$/i);
const formattedCss = screen.getByLabelText(/FormattedCSS/i);

const clearBtns = screen.getAllByRole('button', { name: /Clear/i });

await user.clear(css);
await user.type(css, 'body{}'.replace(/[{[]/g, '$&$&'));
await user.click(clearBtns[0]);

expect(css).toHaveValue('');
expect(formattedCss).toHaveValue('');

await user.type(css, 'body{}'.replace(/[{[]/g, '$&$&'));
await user.click(clearBtns[1]);

expect(css).toHaveValue('');
expect(formattedCss).toHaveValue('');
});
});
217 changes: 217 additions & 0 deletions components/css/CssForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import ClearIcon from '@mui/icons-material/Clear';
import ContentCopyIcon from '@mui/icons-material/ContentCopy';
import ContentPasteGoIcon from '@mui/icons-material/ContentPasteGo';
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Grid from '@mui/material/Grid';
import Switch from '@mui/material/Switch';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import { minify } from 'csso';
import prettier from 'prettier';
import parserCss from 'prettier/parser-postcss';
import React from 'react';

import useSupportsClipboardRead from '../../hooks/useSupportsClipboardRead';
import type { ToastProps } from '../Toast';

export interface CssFormProps {
css: string | void;
formattedCss: string | void;
error: string;
minifyCss: boolean;
setCss: (_css: string | void) => void;
setFormattedCss: (_formattedCss: string | void) => void;
setError: (_error: string) => void;
setMinifyCss: (_minifyCss: boolean) => void;
setToastMessage: (_toastMessage: string) => void;
setToastOpen: (_toastOpen: boolean) => void;
setToastSeverity: (_toastSeverity: ToastProps['severity']) => void;
}

export default function CssForm({
css,
formattedCss,
error,
minifyCss,
setCss,
setFormattedCss,
setError,
setMinifyCss,
setToastMessage,
setToastOpen,
setToastSeverity,
}: CssFormProps) {
const supportsClipboardRead = useSupportsClipboardRead();
function calculateFormattedCss(
value: string | void,
minification: boolean | false,
) {
setCss(value);
if (!value) {
setFormattedCss('');
}
try {
const formatted = prettier.format(value ?? '', {
parser: 'css',
plugins: [parserCss],
});

if (minification) {
const minified = minify(formatted).css;
setFormattedCss(minified);
} else {
setFormattedCss(formatted);
}
setError('');
} catch (e: unknown) {
setError((e as { message: string })?.message ?? '');
setFormattedCss('');
}
}

function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { value, name } = event.target;
if (name === 'css') {
calculateFormattedCss(value, minifyCss);
}
if (name === 'minifySwitch') {
calculateFormattedCss(css, !minifyCss);
setMinifyCss(!minifyCss);
}
}

return (
<Box
display='flex'
flexDirection='column'
justifyContent='stretch'
gap={6}
width={1000}
maxWidth='100%'
>
<Box
display='flex'
flexDirection='column'
gap={2}
>
<TextField
multiline
error={error.length > 0}
label='CSS'
name='css'
onChange={handleChange}
value={css}
/>
<Box
display='flex'
flexWrap='wrap'
justifyContent='end'
gap={2}
>
<Button
startIcon={<ClearIcon />}
disabled={!css}
onClick={() => {
setCss('');
setFormattedCss('');
}}
>
Clear
</Button>
{!!supportsClipboardRead && (
<Button
startIcon={<ContentPasteGoIcon />}
onClick={async () => {
const text = await navigator.clipboard.readText();
if (text) {
setCss(text);
calculateFormattedCss(text, minifyCss);
}
}}
>
Paste
</Button>
)}
</Box>
{error && (
<pre>
<Typography
paragraph
color='#ff6246'
data-testid='css-error'
>
{error}
</Typography>
</pre>
)}
</Box>
<Grid
component='label'
container
alignItems='center'
spacing={1}
>
<Grid item>Format</Grid>
<Grid item>
<Switch
checked={minifyCss}
name='minifySwitch'
onChange={handleChange}
/>
</Grid>
<Grid item>Minify</Grid>
</Grid>
<Box
display='flex'
flexDirection='column'
gap={2}
>
<TextField
multiline
label={minifyCss ? 'MinifiedCSS' : 'FormattedCSS'}
name='outputCss'
value={formattedCss}
/>
<Box
display='flex'
flexWrap='wrap'
justifyContent='end'
gap={2}
>
<Button
startIcon={<ClearIcon />}
disabled={!formattedCss}
onClick={() => {
setFormattedCss('');
setCss('');
}}
>
Clear
</Button>
<Button
startIcon={<ContentCopyIcon />}
disabled={!formattedCss}
name='Copy'
onClick={() => {
navigator.clipboard.writeText(formattedCss || '').then(
() => {
setToastMessage('Copied to clipboard');
setToastSeverity('success');
setToastOpen(true);
},
() => {
setToastMessage('Failed to copy to clipboard');
setToastSeverity('error');
setToastOpen(true);
},
);
}}
>
Copy
</Button>
</Box>
</Box>
</Box>
);
}
6 changes: 6 additions & 0 deletions data/nav.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import AspectRatio from '@mui/icons-material/AspectRatio';
import Code from '@mui/icons-material/Code';
import Css from '@mui/icons-material/Css';
import DataObject from '@mui/icons-material/DataObject';
import Fingerprint from '@mui/icons-material/Fingerprint';
import HomeIcon from '@mui/icons-material/Home';
Expand All @@ -19,6 +20,11 @@ export default [
href: '/aspect-ratio',
Icon: AspectRatio,
},
{
title: 'CSS',
href: '/css',
Icon: Css,
},
{
title: 'UUID',
href: '/uuid',
Expand Down
Loading