mirror of
https://github.com/ianstormtaylor/slate.git
synced 2025-07-31 12:30:11 +02:00
Fix(editable component): reselect the range created by triple click (#4455)
* fix(editable component): reselect the range created by triple click * Revert "fix(editable component): reselect the range created by triple click" Reason: attaching a handler for `onClick` event is no longer needed. * fix(react-editor): reselect DOMSelection when triple clicked Reason: Triple clicking an element in Chrome will falsely set the focus node as the next sibling node with focusOffset 0 * test: add e2e test for triple click
This commit is contained in:
62
cypress/integration/examples/select.ts
Normal file
62
cypress/integration/examples/select.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
describe('selection', () => {
|
||||||
|
// Currently, testing color property always yields rgb() value, even when stored
|
||||||
|
// as hex.
|
||||||
|
// Hence, we'll overwrite Cypress `should` command, in which we use
|
||||||
|
// `getComputedStyle` on both the subject element and creates a temp element
|
||||||
|
// to get the computed color and compares.
|
||||||
|
// Code by Nicholas Boll at https://github.com/cypress-io/cypress/issues/2186
|
||||||
|
// 2021/08/27
|
||||||
|
type CssStyleObject = Partial<CSSStyleDeclaration> &
|
||||||
|
Record<string, string | null>
|
||||||
|
const compareColor = (color: string, property: string) => (
|
||||||
|
targetElement: NodeListOf<Element>
|
||||||
|
) => {
|
||||||
|
const tempElement = document.createElement('div')
|
||||||
|
tempElement.style.color = color
|
||||||
|
tempElement.style.display = 'none' // make sure it doesn't actually render
|
||||||
|
document.body.appendChild(tempElement) // append so that `getComputedStyle` actually works
|
||||||
|
|
||||||
|
const tempColor = getComputedStyle(tempElement).color
|
||||||
|
// Calling window.getComputedStyle(element) returns `CSSStyleDeclaration`
|
||||||
|
// object which has numeric index signature with string keys.
|
||||||
|
// We need to declare a new object which retains the typings of
|
||||||
|
// CSSStyleDeclaration and yet is relaxed enough to accept an
|
||||||
|
// arbitrary property name.
|
||||||
|
|
||||||
|
const targetStyle = getComputedStyle(targetElement[0]) as CssStyleObject
|
||||||
|
const targetColor = targetStyle[property]
|
||||||
|
|
||||||
|
document.body.removeChild(tempElement) // remove it because we're done with it
|
||||||
|
|
||||||
|
expect(tempColor).to.equal(targetColor)
|
||||||
|
}
|
||||||
|
Cypress.Commands.overwrite(
|
||||||
|
'should',
|
||||||
|
(originalFn, subject, expectation, ...args) => {
|
||||||
|
const customMatchers: { [key: string]: any } = {
|
||||||
|
'have.backgroundColor': compareColor(args[0], 'backgroundColor'),
|
||||||
|
'have.color': compareColor(args[0], 'color'),
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if the expectation is a string and if it is a member of Jest's expect
|
||||||
|
if (typeof expectation === 'string' && customMatchers[expectation]) {
|
||||||
|
return originalFn(subject, customMatchers[expectation])
|
||||||
|
}
|
||||||
|
return originalFn(subject, expectation, ...args)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const slateEditor = '[data-slate-node="element"]'
|
||||||
|
beforeEach(() => cy.visit('examples/richtext'))
|
||||||
|
it('select the correct block when triple clicking', () => {
|
||||||
|
// triple clicking the second block (paragraph) shouldn't highlight the
|
||||||
|
// quote button
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
cy.get(slateEditor)
|
||||||
|
.eq(1)
|
||||||
|
.click()
|
||||||
|
}
|
||||||
|
cy.contains('.material-icons', /format_quote/)
|
||||||
|
.parent()
|
||||||
|
.should('have.color', '#ccc')
|
||||||
|
})
|
||||||
|
})
|
@@ -560,6 +560,44 @@ export const ReactEditor = {
|
|||||||
anchorOffset = domRange.anchorOffset
|
anchorOffset = domRange.anchorOffset
|
||||||
focusNode = domRange.focusNode
|
focusNode = domRange.focusNode
|
||||||
focusOffset = domRange.focusOffset
|
focusOffset = domRange.focusOffset
|
||||||
|
// When triple clicking a block, Chrome will return a selection object whose
|
||||||
|
// focus node is the next element sibling and focusOffset is 0.
|
||||||
|
// This will highlight the corresponding toolbar button for the sibling
|
||||||
|
// block even though users just want to target the previous block.
|
||||||
|
// (2021/08/24)
|
||||||
|
// Within the context of Slate and Chrome, if anchor and focus nodes don't have
|
||||||
|
// the same nodeValue and focusOffset is 0, then it's definitely a triple click
|
||||||
|
// behaviour.
|
||||||
|
if (
|
||||||
|
IS_CHROME &&
|
||||||
|
anchorNode?.nodeValue !== focusNode?.nodeValue &&
|
||||||
|
domRange.focusOffset === 0
|
||||||
|
) {
|
||||||
|
// If an anchorNode is an element node when triple clicked, then the focusNode
|
||||||
|
// should also be the same as anchorNode when triple clicked.
|
||||||
|
if (anchorNode!.nodeType === 1) {
|
||||||
|
focusNode = anchorNode
|
||||||
|
} else {
|
||||||
|
// Otherwise, anchorNode is a text node and we need to
|
||||||
|
// - climb up the DOM tree to get the farthest element node that receives
|
||||||
|
// triple click. It should have atribute 'data-slate-node' = "element"
|
||||||
|
// - get the last child of that element node
|
||||||
|
// - climb down the DOM tree to get the text node of the last child
|
||||||
|
// - this is also the end of the selection aka the focusNode
|
||||||
|
const anchorElement = anchorNode!.parentNode as HTMLElement
|
||||||
|
const tripleClickedBlock = anchorElement.closest(
|
||||||
|
'[data-slate-node="element"]'
|
||||||
|
)
|
||||||
|
const focusElement = tripleClickedBlock!.lastElementChild
|
||||||
|
// Get the element node that holds the focus text node
|
||||||
|
const innermostFocusElement = focusElement!.querySelector(
|
||||||
|
'[data-slate-string]'
|
||||||
|
)
|
||||||
|
const lastTextNode = innermostFocusElement!.childNodes[0]
|
||||||
|
focusNode = lastTextNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// COMPAT: There's a bug in chrome that always returns `true` for
|
// COMPAT: There's a bug in chrome that always returns `true` for
|
||||||
// `isCollapsed` for a Selection that comes from a ShadowRoot.
|
// `isCollapsed` for a Selection that comes from a ShadowRoot.
|
||||||
// (2020/08/08)
|
// (2020/08/08)
|
||||||
|
Reference in New Issue
Block a user