fix: fix some react focus issues (#1724), fix #1716, fix #1608, fix #1520

* remove async createNodeViews

* focus asynchronously to fix weird bugs in react
This commit is contained in:
Philipp Kühn 2021-08-12 18:03:45 +02:00 committed by GitHub
parent 812c49bcb1
commit 956566eaad
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 46 additions and 27 deletions

View File

@ -11,10 +11,12 @@ declare module '@tiptap/core' {
} }
} }
export const blur: RawCommands['blur'] = () => ({ view }) => { export const blur: RawCommands['blur'] = () => ({ editor, view }) => {
const element = view.dom as HTMLElement requestAnimationFrame(() => {
if (!editor.isDestroyed) {
element.blur() (view.dom as HTMLElement).blur()
}
})
return true return true
} }

View File

@ -47,13 +47,23 @@ export const focus: RawCommands['focus'] = (position = null) => ({
tr, tr,
dispatch, dispatch,
}) => { }) => {
const delayedFocus = () => {
// For React we have to focus asynchronously. Otherwise wild things happen.
// see: https://github.com/ueberdosis/tiptap/issues/1520
requestAnimationFrame(() => {
if (!editor.isDestroyed) {
view.focus()
}
})
}
if ((view.hasFocus() && position === null) || position === false) { if ((view.hasFocus() && position === null) || position === false) {
return true return true
} }
// we dont try to resolve a NodeSelection or CellSelection // we dont try to resolve a NodeSelection or CellSelection
if (dispatch && position === null && !isTextSelection(editor.state.selection)) { if (dispatch && position === null && !isTextSelection(editor.state.selection)) {
view.focus() delayedFocus()
return true return true
} }
@ -67,7 +77,9 @@ export const focus: RawCommands['focus'] = (position = null) => ({
const isSameSelection = editor.state.selection.eq(selection) const isSameSelection = editor.state.selection.eq(selection)
if (dispatch) { if (dispatch) {
tr.setSelection(selection) if (!isSameSelection) {
tr.setSelection(selection)
}
// `tr.setSelection` resets the stored marks // `tr.setSelection` resets the stored marks
// so well restore them if the selection is the same as before // so well restore them if the selection is the same as before
@ -75,7 +87,7 @@ export const focus: RawCommands['focus'] = (position = null) => ({
tr.setStoredMarks(storedMarks) tr.setStoredMarks(storedMarks)
} }
view.focus() delayedFocus()
} }
return true return true

View File

@ -36,7 +36,7 @@ export class BubbleMenuView {
public preventHide = false public preventHide = false
public tippy!: Instance public tippy: Instance | undefined
public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ state, from, to }) => { public shouldShow: Exclude<BubbleMenuPluginProps['shouldShow'], null> = ({ state, from, to }) => {
const { doc, selection } = state const { doc, selection } = state
@ -74,8 +74,13 @@ export class BubbleMenuView {
this.view.dom.addEventListener('dragstart', this.dragstartHandler) this.view.dom.addEventListener('dragstart', this.dragstartHandler)
this.editor.on('focus', this.focusHandler) this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler) this.editor.on('blur', this.blurHandler)
this.createTooltip(tippyOptions)
this.element.style.visibility = 'visible' this.element.style.visibility = 'visible'
// We create tippy asynchronously to make sure that `editor.options.element`
// has already been moved to the right position in the DOM
requestAnimationFrame(() => {
this.createTooltip(tippyOptions)
})
} }
mousedownHandler = () => { mousedownHandler = () => {
@ -109,7 +114,7 @@ export class BubbleMenuView {
} }
createTooltip(options: Partial<Props> = {}) { createTooltip(options: Partial<Props> = {}) {
this.tippy = tippy(this.view.dom, { this.tippy = tippy(this.editor.options.element, {
duration: 0, duration: 0,
getReferenceClientRect: null, getReferenceClientRect: null,
content: this.element, content: this.element,
@ -150,7 +155,7 @@ export class BubbleMenuView {
return return
} }
this.tippy.setProps({ this.tippy?.setProps({
getReferenceClientRect: () => { getReferenceClientRect: () => {
if (isNodeSelection(state.selection)) { if (isNodeSelection(state.selection)) {
const node = view.nodeDOM(from) as HTMLElement const node = view.nodeDOM(from) as HTMLElement
@ -168,15 +173,15 @@ export class BubbleMenuView {
} }
show() { show() {
this.tippy.show() this.tippy?.show()
} }
hide() { hide() {
this.tippy.hide() this.tippy?.hide()
} }
destroy() { destroy() {
this.tippy.destroy() this.tippy?.destroy()
this.element.removeEventListener('mousedown', this.mousedownHandler) this.element.removeEventListener('mousedown', this.mousedownHandler)
this.view.dom.removeEventListener('dragstart', this.dragstartHandler) this.view.dom.removeEventListener('dragstart', this.dragstartHandler)
this.editor.off('focus', this.focusHandler) this.editor.off('focus', this.focusHandler)

View File

@ -29,7 +29,7 @@ export class FloatingMenuView {
public preventHide = false public preventHide = false
public tippy!: Instance public tippy: Instance | undefined
public shouldShow: Exclude<FloatingMenuPluginProps['shouldShow'], null> = ({ state }) => { public shouldShow: Exclude<FloatingMenuPluginProps['shouldShow'], null> = ({ state }) => {
const { selection } = state const { selection } = state
@ -64,8 +64,13 @@ export class FloatingMenuView {
this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true }) this.element.addEventListener('mousedown', this.mousedownHandler, { capture: true })
this.editor.on('focus', this.focusHandler) this.editor.on('focus', this.focusHandler)
this.editor.on('blur', this.blurHandler) this.editor.on('blur', this.blurHandler)
this.createTooltip(tippyOptions)
this.element.style.visibility = 'visible' this.element.style.visibility = 'visible'
// We create tippy asynchronously to make sure that `editor.options.element`
// has already been moved to the right position in the DOM
requestAnimationFrame(() => {
this.createTooltip(tippyOptions)
})
} }
mousedownHandler = () => { mousedownHandler = () => {
@ -95,7 +100,7 @@ export class FloatingMenuView {
} }
createTooltip(options: Partial<Props> = {}) { createTooltip(options: Partial<Props> = {}) {
this.tippy = tippy(this.view.dom, { this.tippy = tippy(this.editor.options.element, {
duration: 0, duration: 0,
getReferenceClientRect: null, getReferenceClientRect: null,
content: this.element, content: this.element,
@ -130,7 +135,7 @@ export class FloatingMenuView {
return return
} }
this.tippy.setProps({ this.tippy?.setProps({
getReferenceClientRect: () => posToDOMRect(view, from, to), getReferenceClientRect: () => posToDOMRect(view, from, to),
}) })
@ -138,15 +143,15 @@ export class FloatingMenuView {
} }
show() { show() {
this.tippy.show() this.tippy?.show()
} }
hide() { hide() {
this.tippy.hide() this.tippy?.hide()
} }
destroy() { destroy() {
this.tippy.destroy() this.tippy?.destroy()
this.element.removeEventListener('mousedown', this.mousedownHandler) this.element.removeEventListener('mousedown', this.mousedownHandler)
this.editor.off('focus', this.focusHandler) this.editor.off('focus', this.focusHandler)
this.editor.off('blur', this.blurHandler) this.editor.off('blur', this.blurHandler)

View File

@ -63,12 +63,7 @@ export class PureEditorContent extends React.Component<EditorContentProps, Edito
editor.contentComponent = this editor.contentComponent = this
// TODO: alternative to setTimeout? editor.createNodeViews()
setTimeout(() => {
if (!editor.isDestroyed) {
editor.createNodeViews()
}
}, 0)
} }
} }