tab, focus improvements

- remove "focused" block state
- tab navigation respects inputs
- allow to focus contentless blocks
This commit is contained in:
Peter Savchenko 2023-12-20 04:05:55 +03:00
parent 0004b41f8f
commit 25d52b0d46
No known key found for this signature in database
GPG key ID: E68306B1AB0F727C
13 changed files with 145 additions and 176 deletions

View file

@ -6,6 +6,7 @@ export default defineConfig({
},
fixturesFolder: 'test/cypress/fixtures',
screenshotsFolder: 'test/cypress/screenshots',
video: false,
videosFolder: 'test/cypress/videos',
e2e: {
// We've imported your old cypress plugins here.

View file

@ -3,6 +3,9 @@
### 2.29.0
- `New` — Editor Config now has the `style.nonce` attribute that could be used to allowlist editor style tag for Content Security Policy "style-src"
- `New` — Toolbox now will be opened by '/' in empty Block instead of Tab
- `New` — Block Tunes now will be opened by 'CMD+/' instead of Tab in non-empty block
- `New` — Tab now will navigate through Blocks. In last block Tab will navigate to the next page input.
- `Fix` — Passing an empty array via initial data or `blocks.render()` won't break the editor
- `Fix` — Layout did not shrink when a large document cleared in Chrome
- `Fix` — Multiple Tooltip elements creation fixed
@ -11,7 +14,9 @@
- `Fix``blocks.render()` won't lead the `onChange` call in Safari
- `Fix` — Editor wrapper element growing on the Inline Toolbar close
- `Fix` — Fix errors thrown by clicks on a document when the editor is being initialized
- `Fix` — Inline Toolbar sometimes opened in an incorrect position. Now it will be aligned by the left side of the selected text. And won't overflow the right side of the text column.
- `Improvement` — Now you can set focus via arrows/Tab to "contentless" (decorative) blocks like Delimiter that has no inputs.
- `Improvement` — Inline Toolbar sometimes opened in an incorrect position. Now it will be aligned by the left side of the selected text. And won't overflow the right side of the text column.
- `Refactoring``ce-block--focused` class toggling removed as unused.
### 2.28.2

View file

@ -111,7 +111,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
wrapper: 'ce-block',
wrapperStretched: 'ce-block--stretched',
content: 'ce-block__content',
focused: 'ce-block--focused',
selected: 'ce-block--selected',
dropTarget: 'ce-block--drop-target',
};
@ -436,22 +435,6 @@ export default class Block extends EventsDispatcher<BlockEvents> {
return !!this.holder.querySelector(mediaTags.join(','));
}
/**
* Set focused state
*
* @param {boolean} state - 'true' to select, 'false' to remove selection
*/
public set focused(state: boolean) {
this.holder.classList.toggle(Block.CSS.focused, state);
}
/**
* Get Block's focused state
*/
public get focused(): boolean {
return this.holder.classList.contains(Block.CSS.focused);
}
/**
* Set selected state
* We don't need to mark Block as Selected when it is empty

View file

@ -63,7 +63,6 @@ export default class Core {
if ((this.configuration as EditorConfig).autofocus) {
Caret.setToBlock(BlockManager.blocks[0], Caret.positions.START);
BlockManager.highlightCurrentNode();
}
onReady();

View file

@ -93,7 +93,6 @@ export default class BlockEvents extends Module {
const isShortcut = event.ctrlKey || event.metaKey || event.altKey || event.shiftKey;
if (!isShortcut) {
this.Editor.BlockManager.clearFocused();
this.Editor.BlockSelection.clearSelection(event);
}
}
@ -192,49 +191,21 @@ export default class BlockEvents extends Module {
* @param {KeyboardEvent} event - keydown
*/
private tabPressed(event: KeyboardEvent): void {
/**
* Clear blocks selection by tab
*/
this.Editor.BlockSelection.clearSelection();
this.Editor.BlockManager.clearFocused();
const { InlineToolbar, ConversionToolbar, Caret } = this.Editor;
const { BlockManager, InlineToolbar, ConversionToolbar, Caret } = this.Editor;
const isFlipperActivated = ConversionToolbar.opened || InlineToolbar.opened;
if (isFlipperActivated) {
return;
}
/**
* Block to be focused by tab
*/
const nextBlock: Block | null | undefined = BlockManager.nextBlock;
const isNavigated = event.shiftKey ? Caret.navigatePrevious(true) : Caret.navigateNext(true);
/**
* If we have next Block to focus, then focus it. Otherwise, leave native Tab behaviour
* If we have next Block/input to focus, then focus it. Otherwise, leave native Tab behaviour
*/
if (nextBlock !== null) {
if (isNavigated) {
event.preventDefault();
/**
* If next Block is not focusable, just select (highlight) it
*/
if (!nextBlock.focusable) {
/**
* Hide current cursor
*/
window.getSelection()?.removeAllRanges();
/**
* Highlight Block
*/
nextBlock.selected = true;
BlockManager.currentBlock = nextBlock;
return;
} else {
Caret.setToBlock(nextBlock, Caret.positions.START);
}
}
}
@ -535,9 +506,8 @@ export default class BlockEvents extends Module {
}
/**
* Close Toolbar and highlighting when user moves cursor
* Close Toolbar when user moves cursor
*/
this.Editor.BlockManager.clearFocused();
this.Editor.Toolbar.close();
const shouldEnableCBS = this.Editor.Caret.isAtEnd || this.Editor.BlockSelection.anyBlockSelected;
@ -556,19 +526,21 @@ export default class BlockEvents extends Module {
* Default behaviour moves cursor by 1 character, we need to prevent it
*/
event.preventDefault();
} else {
/**
* After caret is set, update Block input index
*/
_.delay(() => {
/** Check currentBlock for case when user moves selection out of Editor */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();
return;
}
/**
* After caret is set, update Block input index
*/
_.delay(() => {
/** Check currentBlock for case when user moves selection out of Editor */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();
/**
* Clear blocks selection by arrows
*/
@ -594,9 +566,8 @@ export default class BlockEvents extends Module {
}
/**
* Close Toolbar and highlighting when user moves cursor
* Close Toolbar when user moves cursor
*/
this.Editor.BlockManager.clearFocused();
this.Editor.Toolbar.close();
const shouldEnableCBS = this.Editor.Caret.isAtStart || this.Editor.BlockSelection.anyBlockSelected;
@ -615,19 +586,21 @@ export default class BlockEvents extends Module {
* Default behaviour moves cursor by 1 character, we need to prevent it
*/
event.preventDefault();
} else {
/**
* After caret is set, update Block input index
*/
_.delay(() => {
/** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();
return;
}
/**
* After caret is set, update Block input index
*/
_.delay(() => {
/** Check currentBlock for case when user ends selection out of Editor and then press arrow-key */
if (this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.updateCurrentInput();
}
// eslint-disable-next-line @typescript-eslint/no-magic-numbers
}, 20)();
/**
* Clear blocks selection by arrows
*/
@ -677,7 +650,6 @@ export default class BlockEvents extends Module {
*/
private activateBlockSettings(): void {
if (!this.Editor.Toolbar.opened) {
this.Editor.BlockManager.currentBlock.focused = true;
this.Editor.Toolbar.moveAndOpen();
}

View file

@ -663,33 +663,6 @@ export default class BlockManager extends Module {
}
}
/**
* Remove selection from all Blocks then highlight only Current Block
*/
public highlightCurrentNode(): void {
/**
* Remove previous selected Block's state
*/
this.clearFocused();
/**
* Mark current Block as selected
*
* @type {boolean}
*/
this.currentBlock.focused = true;
}
/**
* Remove selection from all Blocks
*/
public clearFocused(): void {
this.blocks.forEach((block) => {
block.focused = false;
block.selected = false;
});
}
/**
* 1) Find first-level Block from passed child Node
* 2) Mark it as current
@ -874,7 +847,6 @@ export default class BlockManager extends Module {
*/
public dropPointer(): void {
this.currentBlockIndex = -1;
this.clearFocused();
}
/**

View file

@ -321,26 +321,28 @@ export default class BlockSelection extends Module {
}
/**
* select Block
* Select Block by its index
*
* @param {number?} index - Block index according to the BlockManager's indexes
*/
public selectBlockByIndex(index?): void {
public selectBlockByIndex(index: number): void {
const { BlockManager } = this.Editor;
/**
* Remove previous focused Block's state
*/
BlockManager.clearFocused();
const block = BlockManager.getBlockByIndex(index);
let block;
if (isNaN(index)) {
block = BlockManager.currentBlock;
} else {
block = BlockManager.getBlockByIndex(index);
if (block === undefined) {
return;
}
this.selectBlock(block);
}
/**
* Select passed Block
*
* @param {Block} block - Block to select
*/
public selectBlock(block: Block): void {
/** Save selection */
this.selection.save();
SelectionUtils.get()
@ -354,6 +356,17 @@ export default class BlockSelection extends Module {
this.Editor.InlineToolbar.close();
}
/**
* Remove selection from passed Block
*
* @param {Block} block - Block to unselect
*/
public unselectBlock(block: Block): void {
block.selected = false;
this.clearCache();
}
/**
* Clear anyBlockSelected cache
*/
@ -432,7 +445,7 @@ export default class BlockSelection extends Module {
/**
* select working Block
*/
this.selectBlockByIndex();
this.selectBlock(workingBlock);
/**
* Enable all Blocks selection if current Block is selected

View file

@ -46,8 +46,17 @@ export default class Caret extends Module {
* @returns {boolean}
*/
public get isAtStart(): boolean {
const { currentBlock } = this.Editor.BlockManager;
/**
* If Block does not contain inputs, treat caret as "at start"
*/
if (!currentBlock.focusable) {
return true;
}
const selection = Selection.get();
const firstNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput);
const firstNode = $.getDeepestNode(currentBlock.currentInput);
let focusNode = selection.focusNode;
/** In case lastNode is native input */
@ -138,10 +147,19 @@ export default class Caret extends Module {
* @returns {boolean}
*/
public get isAtEnd(): boolean {
const { currentBlock } = this.Editor.BlockManager;
/**
* If Block does not contain inputs, treat caret as "at end"
*/
if (!currentBlock.focusable) {
return true;
}
const selection = Selection.get();
let focusNode = selection.focusNode;
const lastNode = $.getDeepestNode(this.Editor.BlockManager.currentBlock.currentInput, true);
const lastNode = $.getDeepestNode(currentBlock.currentInput, true);
/** In case lastNode is native input */
if ($.isNativeInput(lastNode)) {
@ -224,7 +242,31 @@ export default class Caret extends Module {
* @param {number} offset - caret offset regarding to the text node
*/
public setToBlock(block: Block, position: string = this.positions.DEFAULT, offset = 0): void {
const { BlockManager } = this.Editor;
const { BlockManager, BlockSelection } = this.Editor;
/**
* Clear previous selection since we possible will select the new Block
*/
BlockSelection.clearSelection();
/**
* If Block is not focusable, just select (highlight) it
*/
if (!block.focusable) {
/**
* Hide current cursor
*/
window.getSelection()?.removeAllRanges();
/**
* Highlight Block
*/
BlockSelection.selectBlock(block);
BlockManager.currentBlock = block;
return;
}
let element;
switch (position) {
@ -388,17 +430,25 @@ export default class Caret extends Module {
* Before moving caret, we should check if caret position is at the end of Plugins node
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
*
* @returns {boolean}
* @param {boolean} force - pass true to skip check for caret position
*/
public navigateNext(): boolean {
const { BlockManager } = this.Editor;
const { currentBlock, nextContentfulBlock } = BlockManager;
public navigateNext(force = false): boolean {
const { BlockManager, BlockSelection } = this.Editor;
const { currentBlock, nextBlock } = BlockManager;
const { nextInput } = currentBlock;
const isAtEnd = this.isAtEnd;
let blockToNavigate = nextBlock;
let nextBlock = nextContentfulBlock;
const navigationAllowed = force || isAtEnd;
if (!nextBlock && !nextInput) {
/** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */
if (nextInput && navigationAllowed) {
this.setToInput(nextInput, this.positions.START);
return true;
}
if (blockToNavigate === null) {
/**
* This code allows to exit from the last non-initial tool:
* https://github.com/codex-team/editor.js/issues/1103
@ -409,7 +459,7 @@ export default class Caret extends Module {
* 2. If there is a last block and it is non-default --> and caret not at the end <--, do nothing
* (https://github.com/codex-team/editor.js/issues/1414)
*/
if (currentBlock.tool.isDefault || !isAtEnd) {
if (currentBlock.tool.isDefault || !navigationAllowed) {
return false;
}
@ -417,16 +467,11 @@ export default class Caret extends Module {
* If there is no nextBlock, but currentBlock is not default,
* insert new default block at the end and navigate to it
*/
nextBlock = BlockManager.insertAtEnd();
blockToNavigate = BlockManager.insertAtEnd() as Block;
}
if (isAtEnd) {
/** If next Tool`s input exists, focus on it. Otherwise set caret to the next Block */
if (!nextInput) {
this.setToBlock(nextBlock, this.positions.START);
} else {
this.setToInput(nextInput, this.positions.START);
}
if (navigationAllowed) {
this.setToBlock(blockToNavigate, this.positions.START);
return true;
}
@ -439,28 +484,27 @@ export default class Caret extends Module {
* Before moving caret, we should check if caret position is start of the Plugins node
* Using {@link Dom#getDeepestNode} to get a last node and match with current selection
*
* @returns {boolean}
* @param {boolean} force - pass true to skip check for caret position
*/
public navigatePrevious(): boolean {
const { currentBlock, previousContentfulBlock } = this.Editor.BlockManager;
public navigatePrevious(force = false): boolean {
const { currentBlock, previousBlock } = this.Editor.BlockManager;
if (!currentBlock) {
return false;
}
const { previousInput } = currentBlock;
const navigationAllowed = force || this.isAtStart;
if (!previousContentfulBlock && !previousInput) {
return false;
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
if (previousInput && navigationAllowed) {
this.setToInput(previousInput, this.positions.END);
return true;
}
if (this.isAtStart) {
/** If previous Tool`s input exists, focus on it. Otherwise set caret to the previous Block */
if (!previousInput) {
this.setToBlock(previousContentfulBlock, this.positions.END);
} else {
this.setToInput(previousInput, this.positions.END);
}
if (previousBlock !== null && navigationAllowed) {
this.setToBlock(previousBlock as Block, this.positions.END);
return true;
}

View file

@ -130,11 +130,6 @@ export default class CrossBlockSelection extends Module {
default:
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
}
} else {
/**
* By default set caret at the end of the last selected block
*/
Caret.setToBlock(BlockManager.blocks[Math.max(fIndex, lIndex)], Caret.positions.END);
}
}

View file

@ -501,7 +501,6 @@ export default class Paste extends Module {
event.preventDefault();
this.processDataTransfer(event.clipboardData);
BlockManager.clearFocused();
Toolbar.close();
};

View file

@ -104,7 +104,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
/**
* Highlight content of a Block we are working with
*/
targetBlock.selected = true;
this.Editor.BlockSelection.selectBlock(targetBlock);
this.Editor.BlockSelection.clearCache();
/**
@ -144,6 +144,10 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
* Close Block Settings pane
*/
public close(): void {
if (!this.opened) {
return;
}
this.opened = false;
/**
@ -163,7 +167,7 @@ export default class BlockSettings extends Module<BlockSettingsNodes> {
* Remove highlighted content of a Block we are working with
*/
if (!this.Editor.CrossBlockSelection.isCrossBlockSelectionStarted && this.Editor.BlockManager.currentBlock) {
this.Editor.BlockManager.currentBlock.selected = false;
this.Editor.BlockSelection.unselectBlock(this.Editor.BlockManager.currentBlock);
}
/** Tell to subscribers that block settings is closed */

View file

@ -249,7 +249,6 @@ export default class UI extends Module<UINodes> {
* @type {Element}
*/
this.nodes.holder = $.getHolder(this.config.holder);
this.nodes.holder.tabIndex = -1;
/**
* Create and save main UI elements
@ -594,11 +593,6 @@ export default class UI extends Module<UINodes> {
this.Editor.Caret.setToBlock(newBlock);
/**
* And highlight
*/
this.Editor.BlockManager.highlightCurrentNode();
/**
* Move toolbar and show plus button because new Block is empty
*/
@ -692,11 +686,6 @@ export default class UI extends Module<UINodes> {
*/
try {
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
/**
* Highlight Current Node
*/
this.Editor.BlockManager.highlightCurrentNode();
} catch (e) {
/**
* If clicked outside first-level Blocks and it is not RectSelection, set Caret to the last empty Block

View file

@ -89,10 +89,3 @@
font-style: italic;
}
}
.codex-editor--narrow .ce-block--focused {
@media (--not-mobile) {
margin-right: calc(var(--narrow-mode-right-padding) * -1);
padding-right: var(--narrow-mode-right-padding);
}
}