Skip to content

Commit

Permalink
Prevent cursor jumps inside contenteditable
Browse files Browse the repository at this point in the history
  • Loading branch information
sventschui committed Aug 17, 2020
1 parent df748d1 commit 0063ca1
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 3 deletions.
25 changes: 25 additions & 0 deletions demo/contenteditable.js
@@ -0,0 +1,25 @@
import { createElement } from 'preact';
import { useState } from 'preact/hooks';

export default function Contenteditable() {
const [value, setValue] = useState("Hey there<br />I'm editable!");

return (
<div>
<div>
<button onClick={() => setValue('')}>Clear!</button>
</div>
<div
style={{
border: '1px solid gray',
padding: '8px',
margin: '8px 0',
background: 'white'
}}
contentEditable
onInput={e => setValue(e.currentTarget.innerHTML)}
dangerouslySetInnerHTML={{ __html: value }}
/>
</div>
);
}
5 changes: 5 additions & 0 deletions demo/index.js
Expand Up @@ -23,6 +23,7 @@ import TextFields from './textFields';
import ReduxBug from './reduxUpdate';
import SuspenseRouterBug from './suspense-router';
import NestedSuspenseBug from './nested-suspense';
import Contenteditable from './contenteditable';
import { MobXDemo } from './mobx';

let isBenchmark = /(\/spiral|\/pythagoras|[#&]bench)/g.test(
Expand Down Expand Up @@ -131,6 +132,9 @@ class App extends Component {
<Link href="/nested-suspense" activeClassName="active">
Nested Suspense Bug
</Link>
<Link href="/contenteditable" activeClassName="active">
contenteditable
</Link>
</nav>
</header>
<main>
Expand Down Expand Up @@ -160,6 +164,7 @@ class App extends Component {
<ReduxBug path="/reduxBug/:start" />
<SuspenseRouterBug path="/suspense-router" />
<NestedSuspenseBug path="/nested-suspense" />
<Contenteditable path="/contenteditable" />
</Router>
</main>
</div>
Expand Down
6 changes: 5 additions & 1 deletion src/diff/index.js
Expand Up @@ -381,7 +381,11 @@ function diffElementNodes(

if (newHtml || oldHtml) {
// Avoid re-applying the same '__html' if it did not changed between re-render
if (!newHtml || !oldHtml || newHtml.__html != oldHtml.__html) {
if (
!newHtml ||
((!oldHtml || newHtml.__html != oldHtml.__html) &&
newHtml.__html !== dom.innerHTML)
) {
dom.innerHTML = (newHtml && newHtml.__html) || '';
}
}
Expand Down
41 changes: 39 additions & 2 deletions test/browser/render.test.js
@@ -1,5 +1,5 @@
import { setupRerender } from 'preact/test-utils';
import { createElement, render, Component, options } from 'preact';
import { createElement, render, Component, options, createRef } from 'preact';
import {
setupScratch,
teardown,
Expand All @@ -8,9 +8,11 @@ import {
serializeHtml,
supportsDataList,
sortAttributes,
spyOnElementAttributes
spyOnElementAttributes,
createEvent
} from '../_util/helpers';
import { clearLog, getLog, logCall } from '../_util/logCall';
import { useState } from 'preact/hooks';

/** @jsx createElement */

Expand Down Expand Up @@ -789,6 +791,41 @@ describe('render()', () => {
expect(checkbox.checked).to.equal(true);
});

it('should always diff `contenteditable` `innerHTML` against the DOM', () => {
// This prevents cursor jumps in contenteditable fields
// See https://github.com/preactjs/preact/issues/2691

function Editable() {
const [value, setValue] = useState('Hello');

return (
<div
contentEditable
dangerouslySetInnerHTML={{ __html: value }}
onInput={e => setValue(e.currentTarget.innerHTML)}
/>
);
}

render(<Editable />, scratch);

let editable = scratch.querySelector('[contenteditable]');
editable.innerHTML = 'World';
editable.dispatchEvent(createEvent('input'));
const range = document.createRange();
range.selectNodeContents(editable);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);

expect(window.getSelection().getRangeAt(0).endOffset).to.equal(1);
rerender();

editable = scratch.querySelector('[contenteditable]');
expect(editable.innerHTML).to.equal('World');
expect(window.getSelection().getRangeAt(0).endOffset).to.equal(1);
});

it('should not re-render when a component returns undefined', () => {
let Dialog = () => undefined;
let updateState;
Expand Down

0 comments on commit 0063ca1

Please sign in to comment.