Skip to content

Commit 1fd3b75

Browse files
committed
Add compareDocumentPosition to fragment instances
1 parent 04bf10e commit 1fd3b75

File tree

2 files changed

+336
-0
lines changed

2 files changed

+336
-0
lines changed

Diff for: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js

+36
Original file line numberDiff line numberDiff line change
@@ -2245,6 +2245,7 @@ export type FragmentInstanceType = {
22452245
getRootNode(getRootNodeOptions?: {
22462246
composed: boolean,
22472247
}): Document | ShadowRoot | FragmentInstanceType,
2248+
compareDocumentPosition(otherNode: Instance): number,
22482249
};
22492250

22502251
function FragmentInstance(this: FragmentInstanceType, fragmentFiber: Fiber) {
@@ -2449,6 +2450,41 @@ FragmentInstance.prototype.getRootNode = function (
24492450
(parentHostInstance.getRootNode(getRootNodeOptions): Document | ShadowRoot);
24502451
return rootNode;
24512452
};
2453+
// $FlowFixMe[prop-missing]
2454+
FragmentInstance.prototype.compareDocumentPosition = function (
2455+
this: FragmentInstanceType,
2456+
otherNode: Instance,
2457+
): number {
2458+
const children: Array<Instance> = [];
2459+
traverseFragmentInstance(this._fragmentFiber, collectChildren, children);
2460+
if (children.length === 0) {
2461+
return (
2462+
Node.DOCUMENT_POSITION_DISCONNECTED |
2463+
Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC
2464+
);
2465+
}
2466+
2467+
const firstElement = children[0];
2468+
const lastElement = children[children.length - 1];
2469+
const firstResult = firstElement.compareDocumentPosition(otherNode);
2470+
const lastResult = lastElement.compareDocumentPosition(otherNode);
2471+
let result;
2472+
2473+
// If otherNode is a child of the Fragment, it should only be
2474+
// Node.DOCUMENT_POSITION_CONTAINED_BY
2475+
if (
2476+
(firstResult & Node.DOCUMENT_POSITION_FOLLOWING &&
2477+
lastResult & Node.DOCUMENT_POSITION_PRECEDING) ||
2478+
otherNode === firstElement ||
2479+
otherNode === lastElement
2480+
) {
2481+
result = Node.DOCUMENT_POSITION_CONTAINED_BY;
2482+
} else {
2483+
result = firstResult;
2484+
}
2485+
2486+
return result;
2487+
};
24522488

24532489
function normalizeListenerOptions(
24542490
opts: ?EventListenerOptionsOrUseCapture,

Diff for: packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js

+300
Original file line numberDiff line numberDiff line change
@@ -966,4 +966,304 @@ describe('FragmentRefs', () => {
966966
expect(fragmentHandle.getRootNode()).toBe(fragmentHandle);
967967
});
968968
});
969+
970+
describe('compareDocumentPosition', () => {
971+
function expectPosition(position, spec) {
972+
expect((position & Node.DOCUMENT_POSITION_PRECEDING) !== 0).toBe(
973+
spec.preceding,
974+
);
975+
expect((position & Node.DOCUMENT_POSITION_FOLLOWING) !== 0).toBe(
976+
spec.following,
977+
);
978+
expect((position & Node.DOCUMENT_POSITION_CONTAINS) !== 0).toBe(
979+
spec.contains,
980+
);
981+
expect((position & Node.DOCUMENT_POSITION_CONTAINED_BY) !== 0).toBe(
982+
spec.containedBy,
983+
);
984+
expect((position & Node.DOCUMENT_POSITION_DISCONNECTED) !== 0).toBe(
985+
spec.disconnected,
986+
);
987+
expect(
988+
(position & Node.DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC) !== 0,
989+
).toBe(spec.implementationSpecific);
990+
}
991+
// @gate enableFragmentRefs
992+
it('returns the relationship between the fragment instance and a given node', async () => {
993+
const fragmentRef = React.createRef();
994+
const beforeRef = React.createRef();
995+
const afterRef = React.createRef();
996+
const middleChildRef = React.createRef();
997+
const firstChildRef = React.createRef();
998+
const lastChildRef = React.createRef();
999+
const containerRef = React.createRef();
1000+
const disconnectedElement = document.createElement('div');
1001+
const root = ReactDOMClient.createRoot(container);
1002+
1003+
function Test() {
1004+
return (
1005+
<div ref={containerRef}>
1006+
<div ref={beforeRef} />
1007+
<React.Fragment ref={fragmentRef}>
1008+
<div ref={firstChildRef} />
1009+
<div ref={middleChildRef} />
1010+
<div ref={lastChildRef} />
1011+
</React.Fragment>
1012+
<div ref={afterRef} />
1013+
</div>
1014+
);
1015+
}
1016+
1017+
await act(() => root.render(<Test />));
1018+
1019+
// document.body is preceding and contains the fragment
1020+
expectPosition(
1021+
fragmentRef.current.compareDocumentPosition(document.body),
1022+
{
1023+
preceding: true,
1024+
following: false,
1025+
contains: true,
1026+
containedBy: false,
1027+
disconnected: false,
1028+
implementationSpecific: false,
1029+
},
1030+
);
1031+
1032+
// beforeRef is preceding the fragment
1033+
expectPosition(
1034+
fragmentRef.current.compareDocumentPosition(beforeRef.current),
1035+
{
1036+
preceding: true,
1037+
following: false,
1038+
contains: false,
1039+
containedBy: false,
1040+
disconnected: false,
1041+
implementationSpecific: false,
1042+
},
1043+
);
1044+
1045+
// afterRef is following the fragment
1046+
expectPosition(
1047+
fragmentRef.current.compareDocumentPosition(afterRef.current),
1048+
{
1049+
preceding: false,
1050+
following: true,
1051+
contains: false,
1052+
containedBy: false,
1053+
disconnected: false,
1054+
implementationSpecific: false,
1055+
},
1056+
);
1057+
1058+
// firstChildRef is contained by the fragment
1059+
expectPosition(
1060+
fragmentRef.current.compareDocumentPosition(firstChildRef.current),
1061+
{
1062+
preceding: false,
1063+
following: false,
1064+
contains: false,
1065+
containedBy: true,
1066+
disconnected: false,
1067+
implementationSpecific: false,
1068+
},
1069+
);
1070+
1071+
// middleChildRef is contained by the fragment
1072+
expectPosition(
1073+
fragmentRef.current.compareDocumentPosition(middleChildRef.current),
1074+
{
1075+
preceding: false,
1076+
following: false,
1077+
contains: false,
1078+
containedBy: true,
1079+
disconnected: false,
1080+
implementationSpecific: false,
1081+
},
1082+
);
1083+
1084+
// lastChildRef is contained by the fragment
1085+
expectPosition(
1086+
fragmentRef.current.compareDocumentPosition(lastChildRef.current),
1087+
{
1088+
preceding: false,
1089+
following: false,
1090+
contains: false,
1091+
containedBy: true,
1092+
disconnected: false,
1093+
implementationSpecific: false,
1094+
},
1095+
);
1096+
1097+
// containerRef preceds and contains the fragment
1098+
expectPosition(
1099+
fragmentRef.current.compareDocumentPosition(containerRef.current),
1100+
{
1101+
preceding: true,
1102+
following: false,
1103+
contains: true,
1104+
containedBy: false,
1105+
disconnected: false,
1106+
implementationSpecific: false,
1107+
},
1108+
);
1109+
1110+
expectPosition(
1111+
fragmentRef.current.compareDocumentPosition(disconnectedElement),
1112+
{
1113+
preceding: false,
1114+
following: true,
1115+
contains: false,
1116+
containedBy: false,
1117+
disconnected: true,
1118+
implementationSpecific: true,
1119+
},
1120+
);
1121+
});
1122+
1123+
// @gate enableFragmentRefs
1124+
it('handles fragment instances with one child', async () => {
1125+
const fragmentRef = React.createRef();
1126+
const beforeRef = React.createRef();
1127+
const afterRef = React.createRef();
1128+
const containerRef = React.createRef();
1129+
const onlyChildRef = React.createRef();
1130+
const disconnectedElement = document.createElement('div');
1131+
const root = ReactDOMClient.createRoot(container);
1132+
1133+
function Test() {
1134+
return (
1135+
<div ref={containerRef}>
1136+
<div ref={beforeRef} />
1137+
<React.Fragment ref={fragmentRef}>
1138+
<div ref={onlyChildRef} />
1139+
</React.Fragment>
1140+
<div ref={afterRef} />
1141+
</div>
1142+
);
1143+
}
1144+
1145+
await act(() => root.render(<Test />));
1146+
expectPosition(
1147+
fragmentRef.current.compareDocumentPosition(beforeRef.current),
1148+
{
1149+
preceding: true,
1150+
following: false,
1151+
contains: false,
1152+
containedBy: false,
1153+
disconnected: false,
1154+
implementationSpecific: false,
1155+
},
1156+
);
1157+
expectPosition(
1158+
fragmentRef.current.compareDocumentPosition(afterRef.current),
1159+
{
1160+
preceding: false,
1161+
following: true,
1162+
contains: false,
1163+
containedBy: false,
1164+
disconnected: false,
1165+
implementationSpecific: false,
1166+
},
1167+
);
1168+
expectPosition(
1169+
fragmentRef.current.compareDocumentPosition(onlyChildRef.current),
1170+
{
1171+
preceding: false,
1172+
following: false,
1173+
contains: false,
1174+
containedBy: true,
1175+
disconnected: false,
1176+
implementationSpecific: false,
1177+
},
1178+
);
1179+
expectPosition(
1180+
fragmentRef.current.compareDocumentPosition(containerRef.current),
1181+
{
1182+
preceding: true,
1183+
following: false,
1184+
contains: true,
1185+
containedBy: false,
1186+
disconnected: false,
1187+
implementationSpecific: false,
1188+
},
1189+
);
1190+
expectPosition(
1191+
fragmentRef.current.compareDocumentPosition(disconnectedElement),
1192+
{
1193+
preceding: false,
1194+
following: true,
1195+
contains: false,
1196+
containedBy: false,
1197+
disconnected: true,
1198+
implementationSpecific: true,
1199+
},
1200+
);
1201+
});
1202+
1203+
// @gate enableFragmentRefs
1204+
it('returns disconnected and implementation specific for any comparison with empty fragment instances', async () => {
1205+
const fragmentRef = React.createRef();
1206+
const beforeRef = React.createRef();
1207+
const afterRef = React.createRef();
1208+
const containerRef = React.createRef();
1209+
const root = ReactDOMClient.createRoot(container);
1210+
1211+
function Test() {
1212+
return (
1213+
<div ref={containerRef}>
1214+
<div ref={beforeRef} />
1215+
<React.Fragment ref={fragmentRef} />
1216+
<div ref={afterRef} />
1217+
</div>
1218+
);
1219+
}
1220+
1221+
await act(() => root.render(<Test />));
1222+
1223+
expectPosition(
1224+
fragmentRef.current.compareDocumentPosition(document.body),
1225+
{
1226+
preceding: false,
1227+
following: false,
1228+
contains: false,
1229+
containedBy: false,
1230+
disconnected: true,
1231+
implementationSpecific: true,
1232+
},
1233+
);
1234+
expectPosition(
1235+
fragmentRef.current.compareDocumentPosition(beforeRef.current),
1236+
{
1237+
preceding: false,
1238+
following: false,
1239+
contains: false,
1240+
containedBy: false,
1241+
disconnected: true,
1242+
implementationSpecific: true,
1243+
},
1244+
);
1245+
expectPosition(
1246+
fragmentRef.current.compareDocumentPosition(afterRef.current),
1247+
{
1248+
preceding: false,
1249+
following: false,
1250+
contains: false,
1251+
containedBy: false,
1252+
disconnected: true,
1253+
implementationSpecific: true,
1254+
},
1255+
);
1256+
expectPosition(
1257+
fragmentRef.current.compareDocumentPosition(containerRef.current),
1258+
{
1259+
preceding: false,
1260+
following: false,
1261+
contains: false,
1262+
containedBy: false,
1263+
disconnected: true,
1264+
implementationSpecific: true,
1265+
},
1266+
);
1267+
});
1268+
});
9691269
});

0 commit comments

Comments
 (0)