mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
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:
committed by
Facebook GitHub Bot
parent
6b2596d187
commit
26fdb44c57
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user