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
27 changes: 27 additions & 0 deletions packages/react-native/Libraries/Components/View/ViewPropTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
LayoutRectangle,
MouseEvent,
PointerEvent,
SafeAreaInsetsChangeEvent,
} from '../../Types/CoreEventTypes';
import type {
AccessibilityActionEvent,
Expand Down Expand Up @@ -63,6 +64,32 @@ type DirectEventProps = Readonly<{
*/
onLayout?: ?(event: LayoutChangeEvent) => unknown,

/**
* Invoked when the part of this view that is covered by the system UI
* (status bar, navigation bar, home indicator, display cutouts, ...)
* changes, with:
*
* `{nativeEvent: {insets: {top, right, bottom, left}, frame: {x, y, width, height}}}`
*
* `insets` are relative to this view: an inset is only non-zero for the part
* of the view that actually overlaps the system UI. `frame` is the position
* of the view at the time of the event, relative to its enclosing view
* controller on iOS and to the window on Android; it does not trigger the
* event on its own, so it can be stale while the view moves without its
* insets changing.
*
* The event is dispatched synchronously, so the rendering it schedules is
* applied in the same frame the insets changed in.
*
* Setting this prop makes the view observe safe area changes; views without
* it are unaffected.
*
* @experimental
*/
experimental_onSafeAreaInsetsChange?: ?(
event: SafeAreaInsetsChangeEvent,
) => unknown,

/**
* When `accessible` is `true`, the system will invoke this function when the
* user performs the magic tap gesture.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow strict-local
* @format
*/

import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';

import type {HostInstance} from 'react-native/src/private/types/HostInstance';

import * as Fantom from '@react-native/fantom';
import * as React from 'react';
import {createRef} from 'react';
import {View} from 'react-native';

const INSETS = {top: 44, right: 0, bottom: 34, left: 0};
const FRAME = {x: 0, y: 0, width: 390, height: 844};

describe('experimental_onSafeAreaInsetsChange', () => {
it('delivers the insets and the frame of the view', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();
const onSafeAreaInsetsChange = jest.fn();

Fantom.runTask(() => {
root.render(
<View
collapsable={false}
ref={nodeRef}
experimental_onSafeAreaInsetsChange={event => {
onSafeAreaInsetsChange(event.nativeEvent);
}}
/>,
);
});

Fantom.dispatchNativeEvent(nodeRef, 'safeAreaInsetsChange', {
insets: INSETS,
frame: FRAME,
});

expect(onSafeAreaInsetsChange).toHaveBeenCalledTimes(1);
const [event] = onSafeAreaInsetsChange.mock.lastCall;
expect(event.insets).toEqual(INSETS);
expect(event.frame).toEqual(FRAME);
});

it('is not delivered to views that did not opt in', () => {
const root = Fantom.createRoot();
const nodeRef = createRef<HostInstance>();

Fantom.runTask(() => {
root.render(<View collapsable={false} ref={nodeRef} />);
});

// The prop is what makes the view observe the safe area, so a view without
// it is never the target of the event.
expect(
root
.getRenderedOutput({props: ['experimental_onSafeAreaInsetsChange']})
.toJSX(),
).toEqual(<rn-view />);
});

it('prevents the view from being flattened', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(
// A layout-only view would ordinarily be flattened away; observing the
// safe area requires a host view to observe with.
<View experimental_onSafeAreaInsetsChange={() => {}}>
<View collapsable={false} />
</View>,
);
});

expect(
root
.getRenderedOutput({props: ['experimental_onSafeAreaInsetsChange']})
.toJSX(),
).toEqual(
<rn-view experimental_onSafeAreaInsetsChange="true">
<rn-view />
</rn-view>,
);
});

it('is reflected in the props of the view when set', () => {
const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(
<View
collapsable={false}
experimental_onSafeAreaInsetsChange={() => {}}
/>,
);
});

expect(
root
.getRenderedOutput({props: ['experimental_onSafeAreaInsetsChange']})
.toJSX(),
).toEqual(<rn-view experimental_onSafeAreaInsetsChange="true" />);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,9 @@ const directEventTypes = {
topLayout: {
registrationName: 'onLayout',
},
topSafeAreaInsetsChange: {
registrationName: 'experimental_onSafeAreaInsetsChange',
},
};

const validAttributesForNonEventProps = {
Expand Down Expand Up @@ -405,6 +408,7 @@ const validAttributesForNonEventProps = {
// Props for bubbling and direct events
const validAttributesForEventProps = {
onLayout: true,
experimental_onSafeAreaInsetsChange: true,

// PanResponder handlers
onMoveShouldSetResponder: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,9 @@ const directEventTypes = {
topLayout: {
registrationName: 'onLayout',
},
topSafeAreaInsetsChange: {
registrationName: 'experimental_onSafeAreaInsetsChange',
},
onGestureHandlerEvent: DynamicallyInjectedByGestureHandler({
registrationName: 'onGestureHandlerEvent',
}),
Expand Down Expand Up @@ -380,6 +383,7 @@ const validAttributesForNonEventProps = {
// Props for bubbling and direct events
const validAttributesForEventProps = ConditionallyIgnoredEventHandlers({
onLayout: true,
experimental_onSafeAreaInsetsChange: true,
onMagicTap: true,

// Accessibility
Expand Down
23 changes: 23 additions & 0 deletions packages/react-native/Libraries/Types/CoreEventTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,29 @@ export type LayoutChangeEvent = NativeSyntheticEvent<
}>,
>;

export type SafeAreaInsets = Readonly<{
top: number,
right: number,
bottom: number,
left: number,
}>;

export type SafeAreaInsetsChangeEvent = NativeSyntheticEvent<
Readonly<{
/**
* The part of the view that is covered by the system UI, in the view's own
* coordinate space.
*/
insets: SafeAreaInsets,
/**
* The frame of the view at the time of the event. Relative to the
* enclosing view controller on iOS and to the window on Android; only
* updated when the insets change.
*/
frame: LayoutRectangle,
}>,
>;

/**
* @deprecated Use `TextLayoutEvent` instead.
*/
Expand Down
31 changes: 0 additions & 31 deletions packages/react-native/React/Fabric/AppleEventBeat.cpp

This file was deleted.

14 changes: 14 additions & 0 deletions packages/react-native/React/Fabric/AppleEventBeat.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

#pragma once

#include <memory>

#include <ReactCommon/RuntimeExecutor.h>
#include <react/renderer/core/EventBeat.h>
#include <react/utils/RunLoopObserver.h>
Expand All @@ -19,6 +21,11 @@ class RuntimeScheduler;
* Event beat associated with JavaScript runtime.
* The beat is called on `RuntimeExecutor`'s thread induced by the UI thread
* event loop.
*
* A synchronous request made while Core Animation is laying out the current
* frame (the run loop observer that induces the beat has already run at that
* point) is additionally induced from the display phase of the same commit
* cycle, so that its effects are mounted before the frame is presented.
*/
class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate {
public:
Expand All @@ -27,13 +34,20 @@ class AppleEventBeat : public EventBeat, public RunLoopObserver::Delegate {
std::unique_ptr<const RunLoopObserver> uiRunLoopObserver,
RuntimeScheduler &RuntimeScheduler);

~AppleEventBeat() override;

void requestSynchronous() const override;

#pragma mark - RunLoopObserver::Delegate

void activityDidChange(const RunLoopObserver::Delegate *delegate, RunLoopObserver::Activity activity)
const noexcept override;

private:
class DisplayPhaseFlusher;

std::unique_ptr<const RunLoopObserver> uiRunLoopObserver_;
std::unique_ptr<DisplayPhaseFlusher> displayPhaseFlusher_;
};

} // namespace facebook::react
Loading
Loading