diff --git a/.gitignore b/.gitignore index db93fc47..150d5070 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,4 @@ dist/ coverage/ .nyc_output/ +.vscode/launch.json diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 6d63e3c4..a1b00479 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -3,6 +3,9 @@ ### 2.30.0 - `Fix` — `onChange` will be called when removing the entire text within a descendant element of a block. +- `Fix` - Unexpected new line on Enter press with selected block without caret +- `Fix` - Search input autofocus loosing after Block Tunes opening +- `Fix` - Block removing while Enter press on Block Tunes ### 2.29.1 diff --git a/package.json b/package.json index 6327cada..3bede118 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@editorjs/editorjs", - "version": "2.30.0-rc.0", + "version": "2.30.0-rc.1", "description": "Editor.js — Native JS, based on API and Open Source", "main": "dist/editorjs.umd.js", "module": "dist/editorjs.mjs", diff --git a/src/components/block/index.ts b/src/components/block/index.ts index 493f9657..25e898f0 100644 --- a/src/components/block/index.ts +++ b/src/components/block/index.ts @@ -738,6 +738,10 @@ export default class Block extends EventsDispatcher { contentNode = $.make('div', Block.CSS.content), pluginsContent = this.toolInstance.render(); + if (import.meta.env.MODE === 'test') { + wrapper.setAttribute('data-cy', 'block-wrapper'); + } + /** * Export id to the DOM three * Useful for standalone modules development. For example, allows to identify Block by some child node. Or scroll to a particular Block by id. diff --git a/src/components/constants.ts b/src/components/constants.ts new file mode 100644 index 00000000..0fe2aac0 --- /dev/null +++ b/src/components/constants.ts @@ -0,0 +1,5 @@ +/** + * Debounce timeout for selection change event + * {@link modules/ui.ts} + */ +export const selectionChangeDebounceTimeout = 180; diff --git a/src/components/modules/crossBlockSelection.ts b/src/components/modules/crossBlockSelection.ts index 5807dc0a..bcebfa4f 100644 --- a/src/components/modules/crossBlockSelection.ts +++ b/src/components/modules/crossBlockSelection.ts @@ -48,11 +48,11 @@ export default class CrossBlockSelection extends Module { } /** - * return boolean is cross block selection started + * Return boolean is cross block selection started: + * there should be at least 2 selected blocks */ public get isCrossBlockSelectionStarted(): boolean { - return !!this.firstSelectedBlock && - !!this.lastSelectedBlock; + return !!this.firstSelectedBlock && !!this.lastSelectedBlock && this.firstSelectedBlock !== this.lastSelectedBlock; } /** diff --git a/src/components/modules/ui.ts b/src/components/modules/ui.ts index e9288363..70e6f2db 100644 --- a/src/components/modules/ui.ts +++ b/src/components/modules/ui.ts @@ -15,6 +15,7 @@ import { mobileScreenBreakpoint } from '../utils'; import styles from '../../styles/main.css?inline'; import { BlockHovered } from '../events/BlockHovered'; +import { selectionChangeDebounceTimeout } from '../constants'; /** * HTML Elements used for UI */ @@ -350,7 +351,6 @@ export default class UI extends Module { /** * Handle selection change to manipulate Inline Toolbar appearance */ - const selectionChangeDebounceTimeout = 180; const selectionChangeDebounced = _.debounce(() => { this.selectionChanged(); }, selectionChangeDebounceTimeout); @@ -556,6 +556,11 @@ export default class UI extends Module { */ private enterPressed(event: KeyboardEvent): void { const { BlockManager, BlockSelection } = this.Editor; + + if (this.someToolbarOpened) { + return; + } + const hasPointerToBlock = BlockManager.currentBlockIndex >= 0; /** @@ -591,6 +596,10 @@ export default class UI extends Module { */ const newBlock = this.Editor.BlockManager.insert(); + /** + * Prevent default enter behaviour to prevent adding a new line (

) to the inserted block + */ + event.preventDefault(); this.Editor.Caret.setToBlock(newBlock); /** diff --git a/src/components/utils/popover/index.ts b/src/components/utils/popover/index.ts index e305afd9..34b483b3 100644 --- a/src/components/utils/popover/index.ts +++ b/src/components/utils/popover/index.ts @@ -237,9 +237,7 @@ export default class Popover extends EventsDispatcher { this.flipper.activate(this.flippableElements); if (this.search !== undefined) { - requestAnimationFrame(() => { - this.search?.focus(); - }); + this.search?.focus(); } if (isMobileScreen()) { diff --git a/test/cypress/support/commands.ts b/test/cypress/support/commands.ts index d680cba2..09a52db8 100644 --- a/test/cypress/support/commands.ts +++ b/test/cypress/support/commands.ts @@ -234,3 +234,30 @@ Cypress.Commands.add('getLineWrapPositions', { return cy.wrap(lineWraps); }); + +/** + * Dispatches keydown event on subject + * Uses the correct KeyboardEvent object to make it work with our code (see below) + */ +Cypress.Commands.add('keydown', { + prevSubject: true, +}, (subject, keyCode: number) => { + cy.log('Dispatching KeyboardEvent with keyCode: ' + keyCode); + /** + * We use the "reason instanceof KeyboardEvent" statement in blockSelection.ts + * but by default cypress' KeyboardEvent is not an instance of the native KeyboardEvent, + * so real-world and Cypress behaviour were different. + * + * To make it work we need to trigger Cypress event with "eventConstructor: 'KeyboardEvent'", + * + * @see https://github.com/cypress-io/cypress/issues/5650 + * @see https://github.com/cypress-io/cypress/pull/8305/files + */ + subject.trigger('keydown', { + eventConstructor: 'KeyboardEvent', + keyCode, + bubbles: false, + }); + + return cy.wrap(subject); +}); diff --git a/test/cypress/support/index.d.ts b/test/cypress/support/index.d.ts index 210895d2..89468b81 100644 --- a/test/cypress/support/index.d.ts +++ b/test/cypress/support/index.d.ts @@ -85,6 +85,14 @@ declare global { * @returns number[] - array of line wrap positions */ getLineWrapPositions(): Chainable; + + /** + * Dispatches keydown event on subject + * Uses the correct KeyboardEvent object to make it work with our code (see below) + * + * @param keyCode - key code to dispatch + */ + keydown(keyCode: number): Chainable; } interface ApplicationWindow { diff --git a/test/cypress/tests/ui/BlockTunes.cy.ts b/test/cypress/tests/ui/BlockTunes.cy.ts new file mode 100644 index 00000000..f652d2c7 --- /dev/null +++ b/test/cypress/tests/ui/BlockTunes.cy.ts @@ -0,0 +1,107 @@ +import { selectionChangeDebounceTimeout } from '../../../../src/components/constants'; + +describe('BlockTunes', function () { + describe('Search', () => { + it('should be focused after popover opened', () => { + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Some text', + }, + }, + ], + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click() + .type('{cmd}/') + .wait(selectionChangeDebounceTimeout); + + /** + * Caret is set to the search input + */ + cy.window() + .then((window) => { + const selection = window.getSelection(); + + expect(selection.rangeCount).to.be.equal(1); + + const range = selection.getRangeAt(0); + + cy.get('[data-cy=editorjs]') + .find('[data-cy="block-tunes"] .cdx-search-field') + .should(($block) => { + expect($block[0].contains(range.startContainer)).to.be.true; + }); + }); + }); + }); + + describe('Keyboard only', function () { + it('should not delete the currently selected block when Enter pressed on a search input (or any block tune)', function () { + const ENTER_KEY_CODE = 13; + + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Some text', + }, + }, + ], + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click() + .type('{cmd}/') + .wait(selectionChangeDebounceTimeout) + .keydown(ENTER_KEY_CODE); + + /** + * Block should have same text + */ + cy.get('[data-cy="block-wrapper"') + .should('have.text', 'Some text'); + }); + + it('should not unselect currently selected block when Enter pressed on a block tune', function () { + const ENTER_KEY_CODE = 13; + + cy.createEditor({ + data: { + blocks: [ + { + type: 'paragraph', + data: { + text: 'Some text', + }, + }, + ], + }, + }); + + cy.get('[data-cy=editorjs]') + .find('.ce-paragraph') + .click() + .type('{cmd}/') + .wait(selectionChangeDebounceTimeout) + .keydown(ENTER_KEY_CODE); + + /** + * Block should not be selected + */ + cy.get('[data-cy="block-wrapper"') + .first() + .should('have.class', 'ce-block--selected'); + }); + }); +});