Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support CodeBlock #3790

Merged
merged 22 commits into from
Aug 10, 2023
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
6 changes: 6 additions & 0 deletions .changeset/serious-poets-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@toptal/picasso-rich-text-editor': major
---

- add support for the code block
- the only breaking change is the need of updating Picasso to ^37.3.0
5 changes: 5 additions & 0 deletions .changeset/silent-carpets-move.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@toptal/picasso-forms': patch
---

- update boundary of rich text editor peer dependency
224 changes: 224 additions & 0 deletions cypress/component/RichTextEditor/CodeBlockPlugin.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import React, { useState } from 'react'
import type { RichTextEditorProps } from '@toptal/picasso-rich-text-editor'
import {
CodePlugin,
CodeBlockPlugin,
RichTextEditor,
} from '@toptal/picasso-rich-text-editor'
import { htmlToHast } from '@toptal/picasso-rich-text-editor/utils'
import { Container } from '@toptal/picasso'

const editorTestId = 'editor'

const defaultProps = {
id: 'foo',
onChange: () => {},
placeholder: 'placeholder',
testIds: {
editor: editorTestId,
},
}

const editorSelector = `#${defaultProps.id}`
const defaultValue = htmlToHast(
'<p>foo <code>bar</code> baz</p><p>qux <code>quux</code> quuz</p>'
)

const Editor = (props: RichTextEditorProps) => {
const [value, setValue] = useState('')

return (
<Container style={{ maxWidth: '600px' }} padded='small'>
<RichTextEditor {...props} onChange={value => setValue(value)} />
<Container padded='small'>{value}</Container>
</Container>
)
}

const component = 'RichTextEditor'

describe('CodeBlockPlugin', () => {
describe('when the cursor is empty line', () => {
it('inserts the code block', () => {
const codeButtonTestId = 'code-button'
const codeBlockButtonTestId = 'code-block-button'

cy.mount(
<Editor
{...{
...defaultProps,
defaultValue,
plugins: [
<CodePlugin testIds={{ button: codeButtonTestId }} />,
<CodeBlockPlugin testIds={{ button: codeBlockButtonTestId }} />,
],
}}
/>
)

cy.get(editorSelector).click()
cy.contains('quuz').click('right')
cy.get(editorSelector).type('{enter}')
cy.getByTestId(codeBlockButtonTestId).as('codeBlockButton').click()
cy.getByTestId(codeButtonTestId).should('be.disabled')

cy.get('body').happoScreenshot({
component,
variant: 'code-block-plugin/new-line',
})
})
})

describe('when the cursor is in the middle of paragraph', () => {
it('turns the paragraph into code block', () => {
const codeBlockButtonTestId = 'code-block-button'

cy.mount(
<Editor
{...{
...defaultProps,
defaultValue,
plugins: [
<CodePlugin />,
<CodeBlockPlugin testIds={{ button: codeBlockButtonTestId }} />,
],
}}
/>
)

cy.get(editorSelector).click()
cy.contains('bar').click()
cy.getByTestId(codeBlockButtonTestId).click()
cy.get('code[dir]').should('exist')

cy.get('body').happoScreenshot({
component,
variant: 'code-block-plugin/middle-of-paragraph-collapsed',
})

cy.getByTestId(codeBlockButtonTestId).click()
cy.get('code[dir]').should('not.exist')

cy.get('body').happoScreenshot({
component,
variant: 'code-block-plugin/middle-of-codeblock-collapsed',
})
})
})

describe('when we select part of the paragraph', () => {
it('turns the selection into code block', () => {
const codeBlockButtonTestId = 'code-block-button'

cy.mount(
<Editor
{...{
...defaultProps,
defaultValue,
plugins: [
<CodePlugin />,
<CodeBlockPlugin testIds={{ button: codeBlockButtonTestId }} />,
],
}}
/>
)

cy.get(editorSelector).click()
// eslint-disable-next-line promise/catch-or-return, max-nested-callbacks, promise/always-return
cy.contains('bar').then($el => {
const el = $el.get(0)
const range = document.createRange()

range.setStart(el.firstChild, 0)
range.setEnd(el.firstChild, 3)

// Clear any existing selections
window.getSelection().removeAllRanges()

// Add the new range to the selection
window.getSelection().addRange(range)
})

cy.getByTestId(codeBlockButtonTestId).click()

cy.get('body').happoScreenshot({
component,
variant: 'code-block-plugin/part-of-paragraph',
})
})
})

describe('when we select part of multiple paragraphs', () => {
it('turns the selection into code block', () => {
const codeBlockButtonTestId = 'code-block-button'

cy.mount(
<Editor
{...{
...defaultProps,
defaultValue,
plugins: [
<CodePlugin />,
<CodeBlockPlugin testIds={{ button: codeBlockButtonTestId }} />,
],
}}
/>
)

cy.get(editorSelector).click()
// eslint-disable-next-line promise/catch-or-return, max-nested-callbacks, promise/always-return
cy.get('p').then(([p1, p2]) => {
const range = document.createRange()

range.setStart(p1.lastChild, 0)
range.setEnd(p2.firstChild, 1)

// Clear any existing selections
window.getSelection().removeAllRanges()

// Add the new range to the selection
window.getSelection().addRange(range)
})

cy.getByTestId(codeBlockButtonTestId).click()

cy.get('body').happoScreenshot({
component,
variant: 'code-block-plugin/part-of-paragraphs',
})
})
})

describe('when we select multilevel list', () => {
it('turns the selection into code block', () => {
const defaultValueWithList = htmlToHast(
`<ul>
<li>
1
<ul>
<li>1.1</li>
</ul>
</li>
</ul>`
)
const codeBlockButtonTestId = 'code-block-button'

cy.mount(
<Editor
{...{
...defaultProps,
defaultValue: defaultValueWithList,
plugins: [
<CodePlugin />,
<CodeBlockPlugin testIds={{ button: codeBlockButtonTestId }} />,
],
}}
/>
)
cy.get(editorSelector).click()
cy.get(editorSelector).type('{selectall}')
cy.getByTestId(codeBlockButtonTestId).click()
cy.get('ul').should('not.exist')
})
})
})
2 changes: 2 additions & 0 deletions jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
global.TextEncoder = require('util').TextEncoder;
global.TextDecoder = require('util').TextDecoder;
2 changes: 1 addition & 1 deletion jest.spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const config = {
'<rootDir>/packages/picasso-rich-text-editor/src/index.ts',
'^@toptal/picasso-root/(.*)$': '<rootDir>/$1',
},
setupFiles: ['jest-canvas-mock'],
setupFiles: ['jest-canvas-mock', './jest.setup.js'],
transformIgnorePatterns: [
'node_modules/(?!@toptal|@topkit|d3|internmap|robust-predicates|delaunator)',
],
Expand Down
2 changes: 1 addition & 1 deletion packages/picasso-forms/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"peerDependencies": {
"@toptal/picasso": "^37.0.0",
"@toptal/picasso-shared": "^12.0.0",
"@toptal/picasso-rich-text-editor": "^4.0.0",
"@toptal/picasso-rich-text-editor": "4 | 5",
"react": ">=16.12.0 < 19.0.0",
"react-dom": ">=16.12.0 < 19.0.0",
"typescript": "~4.7.0"
Expand Down
2 changes: 1 addition & 1 deletion packages/picasso-rich-text-editor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"url": "https://github.com/toptal/picasso/issues"
},
"peerDependencies": {
"@toptal/picasso": "^37.1.0",
"@toptal/picasso": "^37.3.0",
"@toptal/picasso-shared": "^12.0.0",
"@material-ui/core": "4.12.4",
"@lexical/utils": "0.11.2",
Expand Down
13 changes: 10 additions & 3 deletions packages/picasso-rich-text-editor/src/LexicalEditor/styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import type { Theme } from '@material-ui/core/styles'
import type { CSSProperties } from '@material-ui/core/styles/withStyles'
import { rem } from '@toptal/picasso-shared'

import { codeStyles } from '../RichText/components/styles'
import { codeBlockStyles, codeStyles } from '../RichText/components/styles'

const margins = {
'& p': {
'& p, & code[dir]': {
margin: '0.5rem 0',
},
'& h3': {
margin: '1rem 0 0.5rem',
},
'& p:first-child, & h3:first-child': {
'& p:first-child, & h3:first-child, & code[dir]:first-child': {
margin: '0 0 0.5rem',
},
'& li:not(:last-child)': {
Expand Down Expand Up @@ -165,5 +165,12 @@ export default (theme: Theme) => {
},
},
code: codeStyles(theme),
codeBlock: codeBlockStyles(theme),
codeBlockText: {
fontFamily: 'inherit',
fontSize: 'inherit',
lineHeight: 'inherit',
color: 'inherit',
},
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ const testCases = [
],
['<span>', '<h3> > <span>', '<h3><span>test</span></h3>', '<h3>test</h3>'],
['<span>', '<p> > <span>', '<p><span>test</span></p>', '<p>test</p>'],
[
'<span>',
'<pre> > <span>',
'<pre><span>test</span></pre>',
'<pre>test</pre>',
],
[
'<span>',
'<code> > <span>',
'<code><span>test</span></code>',
'<code>test</code>',
],
[
'multiple <span> tags',
'<p> > <span>',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ const replacementMap: ReplaceCondition[] = [
{ parentTag: 'i', childTag: 'em', nodeToRemove: NodeTypes.Parent },
{ parentTag: 'b', childTag: 'strong', nodeToRemove: NodeTypes.Parent },
{ parentTag: 'p', childTag: 'span', nodeToRemove: NodeTypes.Child },
{ parentTag: 'pre', childTag: 'span', nodeToRemove: NodeTypes.Child },
{ parentTag: 'code', childTag: 'span', nodeToRemove: NodeTypes.Child },
{ parentTag: 'h3', childTag: 'span', nodeToRemove: NodeTypes.Child },
{ parentTag: 'li', childTag: 'span', nodeToRemove: NodeTypes.Child },
]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ export const createLexicalTheme = ({
ol: classes.ol,
},
customEmoji: classes.customEmoji,
codeBlock: classes.codeBlock,
codeBlockText: classes.codeBlockText,
}

return theme
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// eslint-disable-next-line import/no-extraneous-dependencies
import { JSDOM } from 'jsdom'

import hasChildDOMNodeTag from './hasChildDOMNodeTag'

describe('hasChildDOMNodeTag', () => {
const { document } = new JSDOM('').window

describe('when node has no children', () => {
it('returns false', () => {
const node = document.createElement('div')

expect(hasChildDOMNodeTag(node, 'SPAN')).toBe(false)
})
})

describe('when node does not have child with specific tag', () => {
it('returns false', () => {
const node = document.createElement('div')
const child = document.createElement('p')

node.appendChild(child)

expect(hasChildDOMNodeTag(node, 'SPAN')).toBe(false)
})
})

describe('when node has a child with the specific tag', () => {
it('returns true', () => {
const node = document.createElement('div')
const child = document.createElement('span')

node.appendChild(child)

expect(hasChildDOMNodeTag(node, 'SPAN')).toBe(true)
})
})

describe('when node has a nested child with the specific tag', () => {
it('returns false', () => {
const node = document.createElement('div')
const child = document.createElement('p')
const nestedChild = document.createElement('span')

child.appendChild(nestedChild)
node.appendChild(child)

expect(hasChildDOMNodeTag(node, 'SPAN')).toBe(false)
})
})
})
Loading