-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
RichText: Fix empty value delete behaviors #8735
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -41,11 +41,11 @@ Render a rich [`contenteditable` input](https://developer.mozilla.org/en-US/docs | |
|
||
### `onMerge( forward: Boolean ): Function` | ||
|
||
*Optional.* Called when blocks can be merged. `forward` is true when merging with the next block, false when merging with the previous block. | ||
*Optional.* Called when blocks can be merged. `forward` is true when merging with the next block, false when merging with the previous block. The callback should return `true` if a merge is possible, or `false` otherwise. This is used in determining whether an empty field should be deleted instead of merged. | ||
|
||
### `onRemove( forward: Boolean ): Function` | ||
|
||
*Optional.* Called when the block can be removed. `forward` is true when the selection is expected to move to the next block, false to the previous block. | ||
*Optional.* Called when the block can be removed. `forward` is true when the selection is expected to move to the next block, false to the previous block. Called when pressing Backspace or Delete on an empty field and only when `onMerge` explicitly returns `false` to indicate non-merge. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here re: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes backed out in rebased d29d5d1. |
||
|
||
### `formattingControls: Array` | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -81,6 +81,7 @@ export class RichText extends Component { | |
this.onFocus = this.onFocus.bind( this ); | ||
this.onChange = this.onChange.bind( this ); | ||
this.onNodeChange = this.onNodeChange.bind( this ); | ||
this.onDeleteKeyDown = this.onDeleteKeyDown.bind( this ); | ||
this.onHorizontalNavigationKeyDown = this.onHorizontalNavigationKeyDown.bind( this ); | ||
this.onKeyDown = this.onKeyDown.bind( this ); | ||
this.onKeyUp = this.onKeyUp.bind( this ); | ||
|
@@ -426,41 +427,84 @@ export class RichText extends Component { | |
} | ||
|
||
/** | ||
* Handles a keydown event from TinyMCE. | ||
* Handles a delete key down event to handle merge or removal for a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be easier to visually scan for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in rebased d29d5d1. |
||
* collapsed selection where caret is at directional edge: forward for a | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An article (eg "the caret") here might improve readability. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added link in rebased d29d5d1. |
||
* delete key down, reverse for a backspace key down. | ||
* | ||
* @param {KeydownEvent} event The keydown event as triggered by TinyMCE. | ||
* @param {tinymce.EditorEvent<KeyboardEvent>} event Keydown event. | ||
*/ | ||
onKeyDown( event ) { | ||
const dom = this.editor.dom; | ||
const rootNode = this.editor.getBody(); | ||
onDeleteKeyDown( event ) { | ||
const { onMerge, onRemove } = this.props; | ||
if ( ! onMerge && ! onRemove ) { | ||
return; | ||
} | ||
|
||
const { keyCode } = event; | ||
const isReverse = keyCode === BACKSPACE; | ||
const { isCollapsed } = getSelection(); | ||
|
||
if ( | ||
getSelection().isCollapsed && ( | ||
( keyCode === BACKSPACE && isHorizontalEdge( rootNode, true ) ) || | ||
( keyCode === DELETE && isHorizontalEdge( rootNode, false ) ) | ||
) | ||
) { | ||
if ( ! this.props.onMerge && ! this.props.onRemove ) { | ||
return; | ||
} | ||
// Only process delete if occurs at uncollapsed edge. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry to nitpick but this comment is a bit unclear... if what occurs? I guess this should be something like: // Only process delete even if the deletion occurs at an uncollapsed edge. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated in rebased d29d5d1. |
||
if ( ! isCollapsed ) { | ||
return; | ||
} | ||
|
||
const forward = keyCode === DELETE; | ||
// It is important to consider emptiness because an empty container | ||
// will include a bogus TinyMCE BR node _after_ the caret, so in a | ||
// forward deletion the isHorizontalEdge function will incorrectly | ||
// interpret the presence of the bogus node as not being at the edge. | ||
const isEmpty = this.isEmpty(); | ||
const isEdge = ( | ||
isEmpty || | ||
isHorizontalEdge( this.editor.getBody(), isReverse ) | ||
); | ||
|
||
if ( this.props.onMerge ) { | ||
this.props.onMerge( forward ); | ||
} | ||
if ( ! isEdge ) { | ||
return; | ||
} | ||
|
||
if ( this.props.onRemove && this.isEmpty() ) { | ||
this.props.onRemove( forward ); | ||
} | ||
let isHandled = false; | ||
|
||
event.preventDefault(); | ||
let isMerged = false; | ||
if ( onMerge ) { | ||
isMerged = onMerge( ! isReverse ); | ||
isHandled = !! isMerged; | ||
} | ||
|
||
// `onMerge`, if it is exists, is not required to return a value, thus | ||
// the check on explicit false. This condition will cover cases where | ||
// either (a) `onMerge` is not defined or (b) it returns false to | ||
// indicate a merge was not possible. | ||
if ( onRemove && false === isMerged && isEmpty ) { | ||
onRemove( ! isReverse ); | ||
isHandled = true; | ||
} | ||
|
||
if ( ! isHandled ) { | ||
// Only prevent default behaviors if handled. | ||
return; | ||
} | ||
|
||
event.preventDefault(); | ||
|
||
// Calling onMerge() or onRemove() will destroy the editor, so it's | ||
// important that we stop other handlers (e.g. ones registered by | ||
// TinyMCE) from also handling this event. | ||
event.stopImmediatePropagation(); | ||
} | ||
|
||
/** | ||
* Handles a keydown event from TinyMCE. | ||
* | ||
* @param {KeydownEvent} event The keydown event as triggered by TinyMCE. | ||
*/ | ||
onKeyDown( event ) { | ||
const dom = this.editor.dom; | ||
const rootNode = this.editor.getBody(); | ||
const { keyCode } = event; | ||
|
||
// Calling onMerge() or onRemove() will destroy the editor, so it's important | ||
// that we stop other handlers (e.g. ones registered by TinyMCE) from | ||
// also handling this event. | ||
event.stopImmediatePropagation(); | ||
const isDelete = keyCode === DELETE || keyCode === BACKSPACE; | ||
if ( isDelete ) { | ||
this.onDeleteKeyDown( event ); | ||
} | ||
|
||
const isHorizontalNavigation = keyCode === LEFT || keyCode === RIGHT; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -77,6 +77,34 @@ async function login() { | |
] ); | ||
} | ||
|
||
/** | ||
* Returns a promise which resolves once it's determined that the active DOM | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, this write-up is great 👍 |
||
* element is not within a RichText field, or the RichText field's TinyMCE has | ||
* completed initialization. This is an unfortunate workaround to address an | ||
* issue where TinyMCE takes its time to become ready for user input. | ||
* | ||
* TODO: This is a code smell, indicating that "too fast" resulting in breakage | ||
* could be equally problematic for a fast human. It should be explored whether | ||
* all event bindings we assign to TinyMCE to handle could be handled through | ||
* the DOM directly instead. | ||
* | ||
* @return {Promise} Promise resolving once RichText is initialized, or is | ||
* determined to not be a container of the active element. | ||
*/ | ||
async function waitForRichTextInitialization() { | ||
const isInRichText = await page.evaluate( () => { | ||
return !! document.activeElement.closest( '.editor-rich-text__tinymce' ); | ||
} ); | ||
|
||
if ( ! isInRichText ) { | ||
return; | ||
} | ||
|
||
return page.waitForFunction( () => { | ||
return !! document.activeElement.closest( '.mce-content-body' ); | ||
} ); | ||
} | ||
|
||
export async function visitAdmin( adminPath, query ) { | ||
await goToWPPath( join( 'wp-admin', adminPath ), query ); | ||
|
||
|
@@ -170,6 +198,7 @@ export async function ensureSidebarOpened() { | |
*/ | ||
export async function clickBlockAppender() { | ||
await expect( page ).toClick( '.editor-default-block-appender__content' ); | ||
await waitForRichTextInitialization(); | ||
} | ||
|
||
/** | ||
|
@@ -199,6 +228,7 @@ export async function insertBlock( searchTerm, panelName = null ) { | |
await panelButton.click(); | ||
} | ||
await page.click( `button[aria-label="${ searchTerm }"]` ); | ||
await waitForRichTextInitialization(); | ||
} | ||
|
||
/** | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a drive-by review nitpick: "true" should be backtick-wrapped (eg "
true
") for improved readability (it's done that way elsewhere).There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changes backed out in rebased d29d5d1.