Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,6 @@ export const EMPTY_OBJ = /** @type {any} */ ({});
export const EMPTY_ARR = [];

export const MATHML_TOKEN_ELEMENTS = /(mi|mn|mo|ms$|mte|msp)/;

export const HAS_MOVE_BEFORE_SUPPORT =
typeof window !== 'undefined' && 'moveBefore' in Element.prototype;
27 changes: 22 additions & 5 deletions src/diff/children.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
INSERT_VNODE,
MATCHED,
UNDEFINED,
NULL
NULL,
HAS_MOVE_BEFORE_SUPPORT
} from '../constants';
import { isArray } from '../util';
import { getDomSibling } from '../component';
Expand Down Expand Up @@ -129,8 +130,9 @@ export function diffChildren(
}

let shouldPlace = childVNode._flags & INSERT_VNODE;
const isMounting = oldVNode == NULL || oldVNode._original == NULL;
if (shouldPlace || oldVNode._children === childVNode._children) {
oldDom = insert(childVNode, oldDom, parentDom, shouldPlace);
oldDom = insert(childVNode, oldDom, parentDom, shouldPlace, isMounting);
} else if (typeof childVNode.type == 'function' && result !== UNDEFINED) {
oldDom = result;
} else if (newDom) {
Expand Down Expand Up @@ -339,9 +341,10 @@ function constructNewChildrenArray(
* @param {PreactElement} oldDom
* @param {PreactElement} parentDom
* @param {number} shouldPlace
* @param {boolean} isMounting
* @returns {PreactElement}
*/
function insert(parentVNode, oldDom, parentDom, shouldPlace) {
function insert(parentVNode, oldDom, parentDom, shouldPlace, isMounting) {
// Note: VNodes in nested suspended trees may be missing _children.
if (typeof parentVNode.type == 'function') {
let children = parentVNode._children;
Expand All @@ -352,7 +355,7 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace) {
// children's _parent pointer to point to the newVNode (parentVNode
// here).
children[i]._parent = parentVNode;
oldDom = insert(children[i], oldDom, parentDom, shouldPlace);
oldDom = insert(children[i], oldDom, parentDom, shouldPlace, false);
}
}

Expand All @@ -362,7 +365,21 @@ function insert(parentVNode, oldDom, parentDom, shouldPlace) {
if (oldDom && parentVNode.type && !oldDom.parentNode) {
oldDom = getDomSibling(parentVNode);
}
parentDom.insertBefore(parentVNode._dom, oldDom || NULL);

const target = oldDom || NULL;
if (HAS_MOVE_BEFORE_SUPPORT && !isMounting) {
try {
// Technically this can throw in multiple scenario's one of which
// is a component that is mounting - we could consider removing
// the condition because we have a catch in place.
// @ts-expect-error This isn't added to TypeScript lib.d.ts yet
parentDom.moveBefore(parentVNode._dom, target);
} catch (error) {
parentDom.insertBefore(parentVNode._dom, target);
}
} else {
parentDom.insertBefore(parentVNode._dom, target);
}
}
oldDom = parentVNode._dom;
}
Expand Down
8 changes: 8 additions & 0 deletions test/_util/logCall.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ export function logCall(obj, method) {
}
break;
}
case 'moveBefore': {
if (args[1] === null && args.length === 2) {
operation = `${serialize(this)}.appendChild(${serialize(args[0])})`;
} else {
operation = `${serialize(this)}.insertBefore(${c})`;
}
break;
}
default: {
operation = `${serialize(this)}.${String(method)}(${c})`;
break;
Expand Down
116 changes: 64 additions & 52 deletions test/browser/focus.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

/* eslint-disable react/jsx-boolean-value */

const supportsMove = 'moveBefore' in Element.prototype;
console.log('SUPPORTS MOVE BEFORE:', supportsMove);

describe('focus', () => {
/** @type {HTMLDivElement} */
let scratch;
Expand Down Expand Up @@ -149,34 +152,39 @@
beforeEach(() => {
scratch = setupScratch();
rerender = setupRerender();
});

Check failure on line 155 in test/browser/focus.test.jsx

View workflow job for this annotation

GitHub Actions / Build & Test / Build & Test

test/browser/focus.test.jsx > focus > should maintain focus when swapping elements

AssertionError: expected +0 to equal 2 - Expected + Received - 2 + 0 ❯ validateFocus test/browser/focus.test.jsx:155:2 ❯ test/browser/focus.test.jsx:186:2

afterEach(() => {
teardown(scratch);
});

it.skip('should maintain focus when swapping elements', () => {
render(
<List>
<Input />
<ListItem>fooo</ListItem>
</List>,
scratch
);
(supportsMove ? it : it.skip)(
'should maintain focus when swapping elements',
() => {
render(
<List>
<Input />
<ListItem>fooo</ListItem>
</List>,
scratch
);

const input = focusInput();
expect(scratch.innerHTML).to.equal(getListHtml([], ['fooo']));
const input = focusInput();
expect(scratch.innerHTML).to.equal(getListHtml([], ['fooo']));

render(
<List>
<ListItem>fooo</ListItem>
<Input />
</List>,
scratch
);
validateFocus(input);
expect(scratch.innerHTML).to.equal(getListHtml(['fooo'], []));
});
render(
<List>
<ListItem>fooo</ListItem>
<Input />
</List>,
scratch
);

expect(scratch.innerHTML).to.equal(getListHtml(['fooo'], []));
// TODO: looks like currently the selection isn't retained when moving
validateFocus(input);
}
);

it('should maintain focus when moving the input around', () => {
function App({ showFirst, showLast }) {
Expand Down Expand Up @@ -470,23 +478,25 @@
return (
<div>
<h1>Heading</h1>
{!this.state.active
? <Fragment>
foobar
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
<input type="text" ref={i => (input = i)} />
{!this.state.active ? (
<Fragment>
foobar
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
<input type="text" ref={i => (input = i)} />
</Fragment>
) : (
<Fragment>
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
: <Fragment>
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
foobar
<input type="text" ref={i => (input = i)} />
</Fragment>}
foobar
<input type="text" ref={i => (input = i)} />
</Fragment>
)}
</div>
);
}
Expand Down Expand Up @@ -524,23 +534,25 @@
return (
<div>
<h1>Heading</h1>
{!this.state.active
? <Fragment>
foobar
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
<input type="text" ref={i => (input = i)} value="foobar" />
{!this.state.active ? (
<Fragment>
foobar
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
<input type="text" ref={i => (input = i)} value="foobar" />
</Fragment>
) : (
<Fragment>
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
: <Fragment>
<Fragment>
Hello World
<h2>yo</h2>
</Fragment>
foobar
<input type="text" ref={i => (input = i)} value="foobar" />
</Fragment>}
foobar
<input type="text" ref={i => (input = i)} value="foobar" />
</Fragment>
)}
</div>
);
}
Expand Down
Loading
Loading