Implement offsetParent, offsetTop and offsetLeft

Summary:
This adds a new method in Fabric to get the offset for an element, and uses it to implement the following methods as defined in  as defined in https://github.com/react-native-community/discussions-and-proposals/pull/607 :
* `offsetParent`: returns the ancestor that's used to position the element relative to. In React Native, this is always the immediate parent, except if the element has `display: none`, in which case is `null`.
* `offsetTop`, `offsetLeft`: returns the position of the outer border of the element, relative to the inner border of its offset parent. This is similar to our existing `measure` function, except that `measure` uses the outer border of the parent, instead of the inner one.

As with `offsetWidth` and `offsetHeight`, these methods return integers instead of double precision values.

Extra: added descriptions to some fields in `LayoutMetrics` :)

Changelog: [internal]

bypass-github-export-checks

Reviewed By: rshest

Differential Revision: D44512594

fbshipit-source-id: 8f05e21f73397db7b88e6841cc117320d1229979
This commit is contained in:
Rubén Norte
2023-04-13 09:19:00 -07:00
committed by Facebook GitHub Bot
parent 6b2596d187
commit 26fdb44c57
5 changed files with 198 additions and 4 deletions
@@ -27,7 +27,10 @@ import {create as createAttributePayload} from '../../ReactNative/ReactFabricPub
import warnForStyleProps from '../../ReactNative/ReactFabricPublicInstance/warnForStyleProps';
import ReadOnlyElement from './ReadOnlyElement';
import ReadOnlyNode from './ReadOnlyNode';
import {getShadowNode} from './ReadOnlyNode';
import {
getPublicInstanceFromInternalInstanceHandle,
getShadowNode,
} from './ReadOnlyNode';
import nullthrows from 'nullthrows';
const noop = () => {};
@@ -59,15 +62,48 @@ export default class ReactNativeElement
}
get offsetLeft(): number {
throw new TypeError('Unimplemented');
const node = getShadowNode(this);
if (node != null) {
const offset = nullthrows(getFabricUIManager()).getOffset(node);
if (offset != null) {
return Math.round(offset[2]);
}
}
return 0;
}
get offsetParent(): ReadOnlyElement | null {
throw new TypeError('Unimplemented');
const node = getShadowNode(this);
if (node != null) {
const offset = nullthrows(getFabricUIManager()).getOffset(node);
if (offset != null) {
const offsetParentInstanceHandle = offset[0];
const offsetParent = getPublicInstanceFromInternalInstanceHandle(
offsetParentInstanceHandle,
);
// $FlowExpectedError[incompatible-type] The value returned by `getOffset` is always an instance handle for `ReadOnlyElement`.
const offsetParentElement: ReadOnlyElement = offsetParent;
return offsetParentElement;
}
}
return null;
}
get offsetTop(): number {
throw new TypeError('Unimplemented');
const node = getShadowNode(this);
if (node != null) {
const offset = nullthrows(getFabricUIManager()).getOffset(node);
if (offset != null) {
return Math.round(offset[1]);
}
}
return 0;
}
get offsetWidth(): number {
@@ -79,6 +79,13 @@ export type Spec = {|
/* width:*/ number,
/* height:*/ number,
],
+getOffset: (
node: Node,
) => ?[
/* offsetParent: */ InternalInstanceHandle,
/* offsetTop: */ number,
/* offsetLeft: */ number,
],
|};
// This is exposed as a getter because apps using the legacy renderer AND
@@ -136,6 +136,13 @@ function* dfs(node: ?Node): Iterator<Node> {
}
}
function hasDisplayNone(node: Node): boolean {
const props = fromNode(node).props;
// Style is flattened when passed to native, so there's no style object.
// $FlowFixMe[prop-missing]
return props != null && props.display === 'none';
}
const FabricUIManagerMock: FabricUIManager = {
createNode: jest.fn(
(
@@ -374,6 +381,56 @@ const FabricUIManagerMock: FabricUIManager = {
return ReadOnlyNode.DOCUMENT_POSITION_FOLLOWING;
}),
getOffset: jest.fn(
(
node: Node,
): ?[
/* offsetParent: */ InternalInstanceHandle,
/* offsetTop: */ number,
/* offsetLeft: */ number,
] => {
const ancestors = getAncestorsInCurrentTree(node);
if (ancestors == null) {
return null;
}
const [parent, position] = ancestors[ancestors.length - 1];
const nodeInCurrentTree = fromNode(parent).children[position];
const currentProps =
nodeInCurrentTree != null ? fromNode(nodeInCurrentTree).props : null;
if (currentProps == null || hasDisplayNone(nodeInCurrentTree)) {
return null;
}
const offsetForTests: ?{
top: number,
left: number,
} =
// $FlowExpectedError[prop-missing]
currentProps.__offsetForTests;
if (offsetForTests == null) {
return null;
}
let currentIndex = ancestors.length - 1;
while (currentIndex >= 0 && !hasDisplayNone(ancestors[currentIndex][0])) {
currentIndex--;
}
if (currentIndex >= 0) {
// The node or one of its ancestors have display: none
return null;
}
return [
fromNode(parent).instanceHandle,
offsetForTests.top,
offsetForTests.left,
];
},
),
};
global.nativeFabricUIManager = FabricUIManagerMock;
@@ -21,14 +21,21 @@ namespace react {
* Describes results of layout process for particular shadow node.
*/
struct LayoutMetrics {
// Origin: relative to its parent content frame (unless using a method that
// computes it relative to other parent or the viewport)
// Size: includes border, padding and content.
Rect frame;
// Width of the border + padding in all directions.
EdgeInsets contentInsets{0};
// Width of the border in all directions.
EdgeInsets borderWidth{0};
DisplayType displayType{DisplayType::Flex};
LayoutDirection layoutDirection{LayoutDirection::Undefined};
Float pointScaleFactor{1.0};
EdgeInsets overflowInset{};
// Origin: the outer border of the node.
// Size: includes content only.
Rect getContentFrame() const {
return Rect{
Point{contentInsets.left, contentInsets.top},
@@ -923,6 +923,93 @@ jsi::Value UIManagerBinding::get(
});
}
if (methodName == "getOffset") {
// This is a method to access offset information for React Native nodes, to
// implement these methods:
// * `HTMLElement.prototype.offsetParent`: see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetParent.
// * `HTMLElement.prototype.offsetTop`: see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetTop.
// * `HTMLElement.prototype.offsetLeft`: see
// https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetLeft.
// It uses the version of the shadow node that is present in the current
// revision of the shadow tree. If the node is not present or is not
// displayed (because any of its ancestors or itself have 'display: none'),
// it returns undefined. Otherwise, it returns its parent (as all nodes in
// React Native are currently "positioned") and its offset relative to its
// parent.
// getOffset(shadowNode: ShadowNode):
// ?[
// /* parent: */ InstanceHandle,
// /* top: */ number,
// /* left: */ number,
// ]
return jsi::Function::createFromHostFunction(
runtime,
name,
1,
[uiManager](
jsi::Runtime &runtime,
jsi::Value const & /*thisValue*/,
jsi::Value const *arguments,
size_t /*count*/) noexcept -> jsi::Value {
auto shadowNode = shadowNodeFromValue(runtime, arguments[0]);
auto newestCloneOfShadowNode =
uiManager->getNewestCloneOfShadowNode(*shadowNode);
auto newestParentOfShadowNode =
uiManager->getNewestParentOfShadowNode(*shadowNode);
// The node is no longer part of an active shadow tree, or it is the
// root node
if (newestCloneOfShadowNode == nullptr ||
newestParentOfShadowNode == nullptr) {
return jsi::Value::undefined();
}
// If the node is not displayed (itself or any of its ancestors has
// "display: none", it returns an empty layout metrics object.
auto layoutMetrics = uiManager->getRelativeLayoutMetrics(
*shadowNode, nullptr, {/* .includeTransform = */ true});
if (layoutMetrics == EmptyLayoutMetrics) {
return jsi::Value::undefined();
}
auto layoutableShadowNode = traitCast<LayoutableShadowNode const *>(
newestCloneOfShadowNode.get());
// This should never happen
if (layoutableShadowNode == nullptr) {
return jsi::Value::undefined();
}
auto layoutableParentShadowNode =
traitCast<LayoutableShadowNode const *>(
newestParentOfShadowNode.get());
// This should never happen
if (layoutableParentShadowNode == nullptr) {
return jsi::Value::undefined();
}
auto originRelativeToParentOuterBorder =
layoutableShadowNode->getLayoutMetrics().frame.origin;
// On the Web, offsets are computed from the inner border of the
// parent.
auto offsetTop = originRelativeToParentOuterBorder.y -
layoutableParentShadowNode->getLayoutMetrics().borderWidth.top;
auto offsetLeft = originRelativeToParentOuterBorder.x -
layoutableParentShadowNode->getLayoutMetrics().borderWidth.left;
return jsi::Array::createWithElements(
runtime,
getInstanceHandleFromShadowNode(
newestParentOfShadowNode, runtime),
jsi::Value{runtime, (double)offsetTop},
jsi::Value{runtime, (double)offsetLeft});
});
}
return jsi::Value::undefined();
}