Merge branch 'master' into devtools-v4-merge

This commit is contained in:
Brian Vaughn
2019-08-26 09:42:25 -07:00
69 changed files with 1989 additions and 696 deletions

View File

@@ -142,6 +142,8 @@ module.exports = {
],
globals: {
SharedArrayBuffer: true,
spyOnDev: true,
spyOnDevAndProd: true,
spyOnProd: true,

View File

@@ -41,6 +41,10 @@
* Warn in Strict Mode if effects are scheduled outside an `act()` call. ([@threepointone](https://github.com/threepointone) in [#15763](https://github.com/facebook/react/pull/15763) and [#16041](https://github.com/facebook/react/pull/16041))
* Warn when using `act` from the wrong renderer. ([@threepointone](https://github.com/threepointone) in [#15756](https://github.com/facebook/react/pull/15756))
### ESLint Plugin: React Hooks
* Report Hook calls at the top level as a violation. ([gaearon](https://github.com/gaearon) in [#16455](https://github.com/facebook/react/pull/16455))
## 16.8.6 (March 27, 2019)
### React DOM

View File

@@ -1,7 +1,7 @@
{
"name": "eslint-plugin-react-hooks",
"description": "ESLint rules for React Hooks",
"version": "1.7.0",
"version": "2.0.1",
"repository": {
"type": "git",
"url": "https://github.com/facebook/react.git",

View File

@@ -432,8 +432,7 @@ export default {
'React Hook function.';
context.report({node: hook, message});
} else if (codePathNode.type === 'Program') {
// We could warn here but there are false positives related
// configuring libraries like `history`.
// These are dangerous if you have inline requires enabled.
const message =
`React Hook "${context.getSource(hook)}" cannot be called ` +
'at the top level. React Hooks must be called in a ' +

View File

@@ -432,7 +432,6 @@ export function mountResponderInstance(
props: Object,
state: Object,
instance: Object,
rootContainerInstance: Object,
) {
throw new Error('Not yet implemented.');
}

View File

@@ -9,6 +9,11 @@
<!-- Upcoming changes go here -->
</details>
## 4.0.6 (August 26, 2019)
#### Bug fixes
* Remove ⚛️ emoji prefix from Firefox extension tab labels
* Standalone polyfills `Symbol` usage
## 4.0.5 (August 19, 2019)
#### Bug fixes
* Props, state, and context values are alpha sorted.

View File

@@ -25,6 +25,7 @@ describe('ReactDOMServerPartialHydration', () => {
ReactFeatureFlags = require('shared/ReactFeatureFlags');
ReactFeatureFlags.enableSuspenseServerRenderer = true;
ReactFeatureFlags.enableSuspenseCallback = true;
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
@@ -1729,4 +1730,169 @@ describe('ReactDOMServerPartialHydration', () => {
// patched up the tree, which might mean we haven't patched the className.
expect(newSpan.className).toBe('hi');
});
it('does not invoke an event on a hydrated node until it commits', async () => {
let suspend = false;
let resolve;
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Sibling({text}) {
if (suspend) {
throw promise;
} else {
return 'Hello';
}
}
let clicks = 0;
function Button() {
let [clicked, setClicked] = React.useState(false);
if (clicked) {
return null;
}
return (
<a
onClick={() => {
setClicked(true);
clicks++;
}}>
Click me
</a>
);
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
<Button />
<Sibling />
</Suspense>
</div>
);
}
suspend = false;
let finalHTML = ReactDOMServer.renderToString(<App />);
let container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
let a = container.getElementsByTagName('a')[0];
// On the client we don't have all data yet but we want to start
// hydrating anyway.
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
expect(container.textContent).toBe('Click meHello');
// We're now partially hydrated.
a.click();
expect(clicks).toBe(0);
// Resolving the promise so that rendering can complete.
suspend = false;
resolve();
await promise;
Scheduler.unstable_flushAll();
jest.runAllTimers();
// TODO: With selective hydration the event should've been replayed
// but for now we'll have to issue it again.
act(() => {
a.click();
});
expect(clicks).toBe(1);
expect(container.textContent).toBe('Hello');
document.body.removeChild(container);
});
it('does not invoke an event on a hydrated EventResponder until it commits', async () => {
let suspend = false;
let resolve;
let promise = new Promise(resolvePromise => (resolve = resolvePromise));
function Sibling({text}) {
if (suspend) {
throw promise;
} else {
return 'Hello';
}
}
const onEvent = jest.fn();
const TestResponder = React.unstable_createResponder('TestEventResponder', {
targetEventTypes: ['click'],
onEvent,
});
function Button() {
let listener = React.unstable_useResponder(TestResponder, {});
return <a listeners={listener}>Click me</a>;
}
function App() {
return (
<div>
<Suspense fallback="Loading...">
<Button />
<Sibling />
</Suspense>
</div>
);
}
suspend = false;
let finalHTML = ReactDOMServer.renderToString(<App />);
let container = document.createElement('div');
container.innerHTML = finalHTML;
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
let a = container.getElementsByTagName('a')[0];
// On the client we don't have all data yet but we want to start
// hydrating anyway.
suspend = true;
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
// We're now partially hydrated.
a.click();
// We should not have invoked the event yet because we're not
// yet hydrated.
expect(onEvent).toHaveBeenCalledTimes(0);
// Resolving the promise so that rendering can complete.
suspend = false;
resolve();
await promise;
Scheduler.unstable_flushAll();
jest.runAllTimers();
// TODO: With selective hydration the event should've been replayed
// but for now we'll have to issue it again.
act(() => {
a.click();
});
expect(onEvent).toHaveBeenCalledTimes(1);
document.body.removeChild(container);
});
});

View File

@@ -13,6 +13,7 @@ let React;
let ReactDOM;
let ReactDOMServer;
let Scheduler;
let act;
// These tests rely both on ReactDOMServer and ReactDOM.
// If a test only needs ReactDOMServer, put it in ReactServerRendering-test instead.
@@ -23,6 +24,7 @@ describe('ReactDOMServerHydration', () => {
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
act = require('react-dom/test-utils').act;
});
it('should have the correct mounting behavior (old hydrate API)', () => {
@@ -499,4 +501,89 @@ describe('ReactDOMServerHydration', () => {
Scheduler.unstable_flushAll();
expect(element.textContent).toBe('Hello world');
});
it('does not invoke an event on a concurrent hydrating node until it commits', () => {
function Sibling({text}) {
Scheduler.unstable_yieldValue('Sibling');
return <span>Sibling</span>;
}
function Sibling2({text}) {
Scheduler.unstable_yieldValue('Sibling2');
return null;
}
let clicks = 0;
function Button() {
Scheduler.unstable_yieldValue('Button');
let [clicked, setClicked] = React.useState(false);
if (clicked) {
return null;
}
return (
<a
onClick={() => {
setClicked(true);
clicks++;
}}>
Click me
</a>
);
}
function App() {
return (
<div>
<Button />
<Sibling />
<Sibling2 />
</div>
);
}
let finalHTML = ReactDOMServer.renderToString(<App />);
let container = document.createElement('div');
container.innerHTML = finalHTML;
expect(Scheduler).toHaveYielded(['Button', 'Sibling', 'Sibling2']);
// We need this to be in the document since we'll dispatch events on it.
document.body.appendChild(container);
let a = container.getElementsByTagName('a')[0];
// Hydrate asynchronously.
let root = ReactDOM.unstable_createRoot(container, {hydrate: true});
root.render(<App />);
// Flush part way through the render.
if (__DEV__) {
// In DEV effects gets double invoked.
expect(Scheduler).toFlushAndYieldThrough(['Button', 'Button', 'Sibling']);
} else {
expect(Scheduler).toFlushAndYieldThrough(['Button', 'Sibling']);
}
expect(container.textContent).toBe('Click meSibling');
// We're now partially hydrated.
a.click();
// Clicking should not invoke the event yet because we haven't committed
// the hydration yet.
expect(clicks).toBe(0);
// Finish the rest of the hydration.
expect(Scheduler).toFlushAndYield(['Sibling2']);
// TODO: With selective hydration the event should've been replayed
// but for now we'll have to issue it again.
act(() => {
a.click();
});
expect(clicks).toBe(1);
expect(container.textContent).toBe('Sibling');
document.body.removeChild(container);
});
});

View File

@@ -23,26 +23,29 @@ export function precacheFiberNode(hostInst, node) {
* ReactDOMTextComponent instance ancestor.
*/
export function getClosestInstanceFromNode(node) {
if (node[internalInstanceKey]) {
return node[internalInstanceKey];
let inst = node[internalInstanceKey];
if (inst) {
return inst;
}
while (!node[internalInstanceKey]) {
if (node.parentNode) {
node = node.parentNode;
do {
node = node.parentNode;
if (node) {
inst = node[internalInstanceKey];
} else {
// Top of the tree. This node must not be part of a React tree (or is
// unmounted, potentially).
return null;
}
}
} while (!inst);
let inst = node[internalInstanceKey];
if (inst.tag === HostComponent || inst.tag === HostText) {
// In Fiber, this will always be the deepest root.
return inst;
let tag = inst.tag;
switch (tag) {
case HostComponent:
case HostText:
// In Fiber, this will always be the deepest root.
return inst;
}
return null;
}

View File

@@ -824,10 +824,9 @@ export function mountResponderInstance(
responderProps: Object,
responderState: Object,
instance: Instance,
rootContainerInstance: Container,
): ReactDOMEventResponderInstance {
// Listen to events
const doc = rootContainerInstance.ownerDocument;
const doc = instance.ownerDocument;
const documentBody = doc.body || doc;
const {
rootEventTypes,

View File

@@ -12,7 +12,7 @@ import {
PASSIVE_NOT_SUPPORTED,
} from 'legacy-events/EventSystemFlags';
import type {AnyNativeEvent} from 'legacy-events/PluginModuleType';
import {HostComponent} from 'shared/ReactWorkTags';
import {HostComponent, SuspenseComponent} from 'shared/ReactWorkTags';
import type {EventPriority} from 'shared/ReactTypes';
import type {
ReactDOMEventResponder,
@@ -32,10 +32,6 @@ import type {Fiber} from 'react-reconciler/src/ReactFiber';
import warning from 'shared/warning';
import {enableFlareAPI} from 'shared/ReactFeatureFlags';
import invariant from 'shared/invariant';
import {
isFiberSuspenseAndTimedOut,
getSuspenseFallbackChild,
} from 'react-reconciler/src/ReactFiberEvents';
import {getClosestInstanceFromNode} from '../client/ReactDOMComponentTree';
import {
@@ -445,7 +441,7 @@ function createDOMResponderEvent(
passive: boolean,
passiveSupported: boolean,
): ReactDOMResponderEvent {
const {pointerType} = (nativeEvent: any);
const {buttons, pointerType} = (nativeEvent: any);
let eventPointerType = '';
let pointerId = null;
@@ -454,7 +450,7 @@ function createDOMResponderEvent(
pointerId = (nativeEvent: any).pointerId;
} else if (nativeEvent.key !== undefined) {
eventPointerType = 'keyboard';
} else if (nativeEvent.button !== undefined) {
} else if (buttons !== undefined) {
eventPointerType = 'mouse';
} else if ((nativeEvent: any).changedTouches !== undefined) {
eventPointerType = 'touch';
@@ -630,6 +626,14 @@ function validateResponderContext(): void {
);
}
function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
}
function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
}
export function dispatchEventForResponderEventSystem(
topLevelType: string,
targetFiber: null | Fiber,

View File

@@ -14,6 +14,7 @@ let ReactFeatureFlags;
let ReactDOM;
let ReactDOMServer;
let ReactTestRenderer;
let Scheduler;
// FIXME: What should the public API be for setting an event's priority? Right
// now it's an enum but is that what we want? Hard coding this for now.
@@ -72,6 +73,7 @@ describe('DOMEventResponderSystem', () => {
React = require('react');
ReactDOM = require('react-dom');
ReactDOMServer = require('react-dom/server');
Scheduler = require('scheduler');
container = document.createElement('div');
document.body.appendChild(container);
});
@@ -811,8 +813,8 @@ describe('DOMEventResponderSystem', () => {
it('the event responder system should warn on accessing invalid properties', () => {
const TestResponder = createEventResponder({
rootEventTypes: ['click'],
onRootEvent: (event, context, props) => {
targetEventTypes: ['click'],
onEvent: (event, context, props) => {
const syntheticEvent = {
target: event.target,
type: 'click',
@@ -823,19 +825,24 @@ describe('DOMEventResponderSystem', () => {
});
let handler;
let buttonRef = React.createRef();
const Test = () => {
const listener = React.unstable_useResponder(TestResponder, {
onClick: handler,
});
return <button listeners={listener}>Click me!</button>;
return (
<button listeners={listener} ref={buttonRef}>
Click me!
</button>
);
};
expect(() => {
handler = event => {
event.preventDefault();
};
ReactDOM.render(<Test />, container);
dispatchClickEvent(document.body);
dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: preventDefault() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -847,7 +854,7 @@ describe('DOMEventResponderSystem', () => {
event.stopPropagation();
};
ReactDOM.render(<Test />, container);
dispatchClickEvent(document.body);
dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: stopPropagation() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -859,7 +866,7 @@ describe('DOMEventResponderSystem', () => {
event.isDefaultPrevented();
};
ReactDOM.render(<Test />, container);
dispatchClickEvent(document.body);
dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: isDefaultPrevented() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -871,7 +878,7 @@ describe('DOMEventResponderSystem', () => {
event.isPropagationStopped();
};
ReactDOM.render(<Test />, container);
dispatchClickEvent(document.body);
dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: isPropagationStopped() is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -883,7 +890,7 @@ describe('DOMEventResponderSystem', () => {
return event.nativeEvent;
};
ReactDOM.render(<Test />, container);
dispatchClickEvent(document.body);
dispatchClickEvent(buttonRef.current);
}).toWarnDev(
'Warning: nativeEvent is not available on event objects created from event responder modules ' +
'(React Flare).' +
@@ -934,4 +941,57 @@ describe('DOMEventResponderSystem', () => {
ReactDOM.render(<Test2 />, container);
buttonRef.current.dispatchEvent(createEvent('foobar'));
});
it('should work with concurrent mode updates', async () => {
const log = [];
const TestResponder = createEventResponder({
targetEventTypes: ['click'],
onEvent(event, context, props) {
log.push(props);
},
});
const ref = React.createRef();
function Test({counter}) {
const listener = React.unstable_useResponder(TestResponder, {counter});
return (
<button listeners={listener} ref={ref}>
Press me
</button>
);
}
let root = ReactDOM.unstable_createRoot(container);
let batch = root.createBatch();
batch.render(<Test counter={0} />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
batch.commit();
// Click the button
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 0}]);
// Clear log
log.length = 0;
// Increase counter
batch = root.createBatch();
batch.render(<Test counter={1} />);
Scheduler.unstable_flushAll();
jest.runAllTimers();
// Click the button again
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 0}]);
// Clear log
log.length = 0;
// Commit
batch.commit();
dispatchClickEvent(ref.current);
expect(log).toEqual([{counter: 1}]);
});
});

View File

@@ -0,0 +1,54 @@
# ContextMenu
The `useContextMenu` hooks responds to context-menu events.
```js
// Example
const Button = (props) => {
const contextmenu = useContextMenu({
disabled,
onContextMenu,
preventDefault
});
return (
<div listeners={contextmenu}>
{props.children}
</div>
);
};
```
## Types
```js
type ContextMenuEvent = {
altKey: boolean,
buttons: 0 | 1 | 2,
ctrlKey: boolean,
metaKey: boolean,
pageX: number,
pageY: number,
pointerType: PointerType,
shiftKey: boolean,
target: Element,
timeStamp: number,
type: 'contextmenu',
x: number,
y: number,
}
```
## Props
### disabled: boolean = false
Disables the responder.
### onContextMenu: (e: ContextMenuEvent) => void
Called when the user performs a gesture to display a context menu.
### preventDefault: boolean = true
Prevents the native behavior (i.e., context menu).

View File

@@ -1,29 +1,29 @@
# Focus
The `Focus` module responds to focus and blur events on its child. Focus events
The `useFocus` hook responds to focus and blur events on its child. Focus events
are dispatched for all input types, with the exception of `onFocusVisibleChange`
which is only dispatched when focusing with a keyboard.
Focus events do not propagate between `Focus` event responders.
Focus events do not propagate between `useFocus` event responders.
```js
// Example
const Button = (props) => {
const [ focusVisible, setFocusVisible ] = useState(false);
const [ isFocusVisible, setFocusVisible ] = useState(false);
const focus = useFocus({
onBlur={props.onBlur}
onFocus={props.onFocus}
onFocusVisibleChange={setFocusVisible}
});
return (
<Focus
onBlur={props.onBlur}
onFocus={props.onFocus}
onFocusVisibleChange={setFocusVisible}
<button
children={props.children}
listeners={focus}
style={{
...(isFocusVisible && focusVisibleStyles)
}}
>
<button
children={props.children}
style={{
...(focusVisible && focusVisibleStyles)
}}
>
</Focus>
);
};
```
@@ -33,6 +33,8 @@ const Button = (props) => {
```js
type FocusEvent = {
target: Element,
pointerType: 'mouse' | 'touch' | 'pen' | 'keyboard',
timeStamp: number,
type: 'blur' | 'focus' | 'focuschange' | 'focusvisiblechange'
}
```
@@ -41,7 +43,7 @@ type FocusEvent = {
### disabled: boolean = false
Disables all `Focus` events.
Disables the responder.
### onBlur: (e: FocusEvent) => void

View File

@@ -1,48 +1,39 @@
# FocusWithin
The `FocusWithin` module responds to focus and blur events on its child. Focus events
The `useFocusWithin` hooks responds to focus and blur events on its child. Focus events
are dispatched for all input types, with the exception of `onFocusVisibleChange`
which is only dispatched when focusing with a keyboard.
Focus events do not propagate between `FocusWithin` event responders.
Focus events do not propagate between `useFocusWithin` event responders.
```js
// Example
const Button = (props) => {
const [ focusWithin, updateFocusWithin ] = useState(false);
const [ focusWithinVisible, updateFocusWithinVisible ] = useState(false);
const [ isFocusWithin, updateFocusWithin ] = useState(false);
const [ isFocusWithinVisible, updateFocusWithinVisible ] = useState(false);
const focusWithin = useFocusWithin({
onFocusWithinChange={updateFocusWithin}
onFocusWithinVisibleChange={updateFocusWithinVisible}
});
return (
<FocusWithin
onFocusWithinChange={updateFocusWithin}
onFocusWithinVisibleChange={updateFocusWithinVisible}
<button
children={props.children}
listeners={focusWithin}
style={{
...(isFocusWithin && focusWithinStyles),
...(isFocusWithinVisible && focusWithinVisibleStyles)
}}
>
<button
children={props.children}
style={{
...(focusWithin && focusWithinStyles),
...(focusWithinVisible && focusWithinVisibleStyles)
}}
>
</FocusWithin>
);
};
```
## Types
```js
type FocusEvent = {
target: Element,
type: 'focuswithinchange' | 'focuswithinvisiblechange'
}
```
## Props
### disabled: boolean = false
Disables all `FocusWithin` events.
Disables the responder.
### onFocusWithinChange: boolean => void

View File

@@ -1,26 +1,29 @@
# Hover
The `Hover` module responds to hover events on the element it wraps. Hover
The `useHover` hook responds to hover events on the element it wraps. Hover
events are only dispatched for `mouse` and `pen` pointer types. Hover begins
when the pointer enters the element's bounds and ends when the pointer leaves.
Hover events do not propagate between `Hover` event responders.
Hover events do not propagate between `useHover` event responders.
```js
// Example
const Link = (props) => (
const [ hovered, setHovered ] = useState(false);
const [ isHovered, setHovered ] = useState(false);
const hover = useHover({
onHoverChange: setHovered
});
return (
<Hover onHoverChange={setHovered}>
<a
{...props}
href={props.href}
style={{
...props.style,
textDecoration: hovered ? 'underline': 'none'
}}
/>
</Hover>
<a
{...props}
href={props.href}
listeners={hover}
style={{
...props.style,
textDecoration: isHovered ? 'underline': 'none'
}}
/>
);
);
```
@@ -28,18 +31,27 @@ const Link = (props) => (
## Types
```js
type HoverEvent = {
pointerType: 'mouse' | 'pen',
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
type HoverEvent = {|
clientX: number,
clientY: number,
pageX: number,
pageY: number,
pointerType: PointerType,
target: Element,
type: 'hoverstart' | 'hoverend' | 'hovermove' | 'hoverchange'
}
timeStamp: number,
type: HoverEventType,
x: number,
y: number,
|};
```
## Props
### disabled: boolean
Disables all `Hover` events.
Disables the responder.
### onHoverChange: boolean => void

View File

@@ -1,33 +1,34 @@
# Press
The `Press` module responds to press events on the element it wraps. Press
The `usePress` hook responds to press events on the element it wraps. Press
events are dispatched for `mouse`, `pen`, `touch`, `trackpad`, and `keyboard`
pointer types. Press events are only dispatched for keyboards when pressing the
Enter or Spacebar keys. If `onPress` is not called, this signifies that the
press ended outside of the element hit bounds (i.e., the user aborted the
press).
Press events do not propagate between `Press` event responders.
Press events do not propagate between `usePress` event responders.
```js
// Example
const Button = (props) => (
const [ pressed, setPressed ] = useState(false);
const [ isPressed, setPressed ] = useState(false);
const press = usePress({
onPress={props.onPress}
onPressChange={setPressed}
});
return (
<Press
onPress={props.onPress}
onPressChange={setPressed}
>
<div
{...props}
role="button"
tabIndex={0}
style={
...buttonStyles,
...(pressed && pressedStyles)
}}
/>
</Press>
<div
{...props}
listeners={press}
role="button"
tabIndex={0}
style={
...buttonStyles,
...(isPressed && pressedStyles)
}}
/>
);
);
```
@@ -37,6 +38,7 @@ const Button = (props) => (
```js
type PressEvent = {
altKey: boolean,
buttons: 0 | 1 | 4,
ctrlKey: boolean,
defaultPrevented: boolean,
metaKey: boolean,
@@ -76,12 +78,7 @@ type PressOffset = {
### disabled: boolean = false
Disables all `Press` events.
### onContextMenu: (e: PressEvent) => void
Called when the context menu is shown. When a press is active, the context menu
will only be shown (and the press cancelled) if `preventDefault` is `false`.
Disables the responder.
### onPress: (e: PressEvent) => void
@@ -115,11 +112,6 @@ down) can be moved back within the bounds of the element to reactivate it.
Ensure you pass in a constant to reduce memory allocations. Default is `20` for
each offset.
### preventContextMenu: boolean = false
Prevents the native context menu from being shown, but `onContextMenu`
is still called.
### preventDefault: boolean = true
Whether to `preventDefault()` native events. Native behavior is prevented by

View File

@@ -28,20 +28,19 @@ type ContextMenuState = {
};
type ContextMenuEvent = {|
target: Element | Document,
type: 'contextmenu',
pointerType: PointerType,
timeStamp: number,
clientX: null | number,
clientY: null | number,
pageX: null | number,
pageY: null | number,
x: null | number,
y: null | number,
altKey: boolean,
buttons: 0 | 1 | 2,
ctrlKey: boolean,
metaKey: boolean,
pageX: null | number,
pageY: null | number,
pointerType: PointerType,
shiftKey: boolean,
target: Element | Document,
timeStamp: number,
type: 'contextmenu',
x: null | number,
y: null | number,
|};
const hasPointerEvents =
@@ -60,13 +59,13 @@ function dispatchContextMenuEvent(
const gestureState = {
altKey: nativeEvent.altKey,
button: nativeEvent.button === 0 ? 'primary' : 'auxillary',
ctrlKey: nativeEvent.altKey,
metaKey: nativeEvent.altKey,
pageX: nativeEvent.altKey,
pageY: nativeEvent.altKey,
buttons: nativeEvent.buttons != null ? nativeEvent.buttons : 0,
ctrlKey: nativeEvent.ctrlKey,
metaKey: nativeEvent.metaKey,
pageX: nativeEvent.pageX,
pageY: nativeEvent.pageY,
pointerType,
shiftKey: nativeEvent.altKey,
shiftKey: nativeEvent.shiftKey,
target,
timeStamp,
type: 'contextmenu',
@@ -121,7 +120,7 @@ export const ContextMenuResponder = React.unstable_createResponder(
contextMenuImpl,
);
export function useContextMenuResponder(
export function useContextMenu(
props: ContextMenuProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(ContextMenuResponder, props);

View File

@@ -244,7 +244,7 @@ export const DragResponder = React.unstable_createResponder(
dragResponderImpl,
);
export function useDragResponder(
export function useDrag(
props: DragProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(DragResponder, props);

View File

@@ -348,7 +348,7 @@ export const FocusResponder = React.unstable_createResponder(
focusResponderImpl,
);
export function useFocusResponder(
export function useFocus(
props: FocusProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(FocusResponder, props);
@@ -485,7 +485,7 @@ export const FocusWithinResponder = React.unstable_createResponder(
focusWithinResponderImpl,
);
export function useFocusWithinResponder(
export function useFocusWithin(
props: FocusWithinProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(FocusWithinResponder, props);

View File

@@ -37,16 +37,16 @@ type HoverState = {
type HoverEventType = 'hoverstart' | 'hoverend' | 'hoverchange' | 'hovermove';
type HoverEvent = {|
pointerType: PointerType,
target: Element | Document,
type: HoverEventType,
timeStamp: number,
clientX: null | number,
clientY: null | number,
pageX: null | number,
pageY: null | number,
pointerType: PointerType,
screenX: null | number,
screenY: null | number,
target: Element | Document,
timeStamp: number,
type: HoverEventType,
x: null | number,
y: null | number,
|};
@@ -341,7 +341,7 @@ export const HoverResponder = React.unstable_createResponder(
hasPointerEvents ? hoverResponderImpl : hoverResponderFallbackImpl,
);
export function useHoverResponder(
export function useHover(
props: HoverProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(HoverResponder, props);

View File

@@ -215,7 +215,7 @@ export const InputResponder = React.unstable_createResponder(
inputResponderImpl,
);
export function useInputResponder(
export function useInput(
props: InputResponderProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(InputResponder, props);

View File

@@ -209,7 +209,7 @@ export const KeyboardResponder = React.unstable_createResponder(
keyboardResponderImpl,
);
export function useKeyboardResponder(
export function useKeyboard(
props: KeyboardProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(KeyboardResponder, props);

View File

@@ -43,6 +43,7 @@ type PressState = {
y: number,
|}>,
addedRootEvents: boolean,
buttons: 0 | 1 | 4,
isActivePressed: boolean,
isActivePressStart: boolean,
isPressed: boolean,
@@ -75,24 +76,24 @@ type PressEventType =
| 'presschange';
type PressEvent = {|
button: 'primary' | 'auxillary',
defaultPrevented: boolean,
target: Element | Document,
type: PressEventType,
pointerType: PointerType,
timeStamp: number,
altKey: boolean,
buttons: 0 | 1 | 4,
clientX: null | number,
clientY: null | number,
ctrlKey: boolean,
defaultPrevented: boolean,
metaKey: boolean,
pageX: null | number,
pageY: null | number,
pointerType: PointerType,
screenX: null | number,
screenY: null | number,
shiftKey: boolean,
target: Element | Document,
timeStamp: number,
type: PressEventType,
x: null | number,
y: null | number,
altKey: boolean,
ctrlKey: boolean,
metaKey: boolean,
shiftKey: boolean,
|};
const hasPointerEvents =
@@ -149,9 +150,9 @@ function createPressEvent(
event: ?ReactDOMResponderEvent,
touchEvent: null | Touch,
defaultPrevented: boolean,
state: PressState,
): PressEvent {
const timeStamp = context.getTimeStamp();
let button = 'primary';
let clientX = null;
let clientY = null;
let pageX = null;
@@ -173,29 +174,26 @@ function createPressEvent(
if (eventObject) {
({clientX, clientY, pageX, pageY, screenX, screenY} = eventObject);
}
if (nativeEvent.button === 1) {
button = 'auxillary';
}
}
return {
button,
defaultPrevented,
target,
type,
pointerType,
timeStamp,
altKey,
buttons: state.buttons,
clientX,
clientY,
ctrlKey,
defaultPrevented,
metaKey,
pageX,
pageY,
pointerType,
screenX,
screenY,
shiftKey,
target,
timeStamp,
type,
x: clientX,
y: clientY,
altKey,
ctrlKey,
metaKey,
shiftKey,
};
}
@@ -221,6 +219,7 @@ function dispatchEvent(
event,
touchEvent,
defaultPrevented,
state,
);
context.dispatchEvent(syntheticEvent, listener, eventPriority);
}
@@ -488,6 +487,7 @@ const pressResponderImpl = {
return {
activationPosition: null,
addedRootEvents: false,
buttons: 0,
isActivePressed: false,
isActivePressStart: false,
isPressed: false,
@@ -581,11 +581,12 @@ const pressResponderImpl = {
state.activePointerId = touchEvent.identifier;
}
// Ignore any device buttons except primary/auxillary and touch/pen contact.
// Ignore any device buttons except primary/middle and touch/pen contact.
// Additionally we ignore primary-button + ctrl-key with Macs as that
// acts like right-click and opens the contextmenu.
if (
nativeEvent.button > 1 ||
nativeEvent.buttons === 2 ||
nativeEvent.buttons > 4 ||
(isMac && isMouseEvent && nativeEvent.ctrlKey)
) {
return;
@@ -600,6 +601,7 @@ const pressResponderImpl = {
}
state.responderRegionOnDeactivation = null;
state.isPressWithinResponderRegion = true;
state.buttons = nativeEvent.buttons;
dispatchPressStartEvents(event, context, props, state);
addRootEventTypes(context, state);
} else {
@@ -703,7 +705,7 @@ const pressResponderImpl = {
case 'mouseup':
case 'touchend': {
if (isPressed) {
const button = nativeEvent.button;
const buttons = state.buttons;
let isKeyboardEvent = false;
let touchEvent;
if (type === 'pointerup' && activePointerId !== pointerId) {
@@ -722,7 +724,7 @@ const pressResponderImpl = {
}
isKeyboardEvent = true;
removeRootEventTypes(context, state);
} else if (button === 1) {
} else if (buttons === 4) {
// Remove the root events here as no 'click' event is dispatched when this 'button' is pressed.
removeRootEventTypes(context, state);
}
@@ -780,7 +782,7 @@ const pressResponderImpl = {
}
}
if (state.isPressWithinResponderRegion && button !== 1) {
if (state.isPressWithinResponderRegion && buttons !== 4) {
dispatchEvent(
event,
onPress,
@@ -848,7 +850,7 @@ export const PressResponder = React.unstable_createResponder(
pressResponderImpl,
);
export function usePressResponder(
export function usePress(
props: PressProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(PressResponder, props);

View File

@@ -310,7 +310,7 @@ export const ScrollResponder = React.unstable_createResponder(
scrollResponderImpl,
);
export function useScrollResponder(
export function useScroll(
props: ScrollProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(ScrollResponder, props);

View File

@@ -281,8 +281,8 @@ export const SwipeResponder = React.unstable_createResponder(
swipeResponderImpl,
);
export function useSwipeListener(
export function useSwipe(
props: SwipeProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useListener(SwipeResponder, props);
return React.unstable_useResponder(SwipeResponder, props);
}

View File

@@ -9,12 +9,17 @@
'use strict';
import {createEventTarget, platform, setPointerEvent} from '../testing-library';
import {
buttonsType,
createEventTarget,
platform,
setPointerEvent,
} from '../testing-library';
let React;
let ReactFeatureFlags;
let ReactDOM;
let useContextMenuResponder;
let useContextMenu;
function initializeModules(hasPointerEvents) {
setPointerEvent(hasPointerEvents);
@@ -23,8 +28,7 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
useContextMenuResponder = require('react-events/context-menu')
.useContextMenuResponder;
useContextMenu = require('react-events/context-menu').useContextMenu;
}
const forcePointerEvents = true;
@@ -51,7 +55,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
const preventDefault = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = useContextMenuResponder({onContextMenu});
const listener = useContextMenu({onContextMenu});
return <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -61,7 +65,11 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
expect.objectContaining({
buttons: buttonsType.secondary,
pointerType: 'mouse',
type: 'contextmenu',
}),
);
});
@@ -70,7 +78,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
const preventDefault = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = useContextMenuResponder({onContextMenu});
const listener = useContextMenu({onContextMenu});
return <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -80,7 +88,11 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
expect(preventDefault).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'touch', type: 'contextmenu'}),
expect.objectContaining({
buttons: buttonsType.none,
pointerType: 'touch',
type: 'contextmenu',
}),
);
});
@@ -88,7 +100,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
const onContextMenu = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = useContextMenuResponder({
const listener = useContextMenu({
onContextMenu,
disabled: true,
});
@@ -106,7 +118,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
const onContextMenu = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = useContextMenuResponder({
const listener = useContextMenu({
onContextMenu,
preventDefault: false,
});
@@ -135,7 +147,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
const onContextMenu = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = useContextMenuResponder({onContextMenu});
const listener = useContextMenu({onContextMenu});
return <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -144,7 +156,11 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
target.contextmenu({}, {modified: true});
expect(onContextMenu).toHaveBeenCalledTimes(1);
expect(onContextMenu).toHaveBeenCalledWith(
expect.objectContaining({pointerType: 'mouse', type: 'contextmenu'}),
expect.objectContaining({
buttons: buttonsType.primary,
pointerType: 'mouse',
type: 'contextmenu',
}),
);
});
});
@@ -163,7 +179,7 @@ describe.each(table)('ContextMenu responder', hasPointerEvents => {
const onContextMenu = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = useContextMenuResponder({onContextMenu});
const listener = useContextMenu({onContextMenu});
return <div ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);

View File

@@ -12,7 +12,7 @@
let React;
let ReactFeatureFlags;
let ReactDOM;
let useDragResponder;
let useDrag;
describe('Drag event responder', () => {
let container;
@@ -23,7 +23,7 @@ describe('Drag event responder', () => {
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
useDragResponder = require('react-events/drag').useDragResponder;
useDrag = require('react-events/drag').useDrag;
container = document.createElement('div');
document.body.appendChild(container);
@@ -44,7 +44,7 @@ describe('Drag event responder', () => {
}
function Component() {
const listener = useDragResponder({
const listener = useDrag({
onDragChange: handleOnDrag,
});
return (
@@ -100,7 +100,7 @@ describe('Drag event responder', () => {
}
function Component() {
const listener = useDragResponder({
const listener = useDrag({
onDragStart: handleDragStart,
onDragEnd: handleDragEnd,
});
@@ -153,7 +153,7 @@ describe('Drag event responder', () => {
}
function Component() {
const listener = useDragResponder({
const listener = useDrag({
onDragMove: handleDragMove,
});
return (

View File

@@ -15,7 +15,7 @@ let React;
let ReactFeatureFlags;
let ReactDOM;
let FocusResponder;
let useFocusResponder;
let useFocus;
function initializeModules(hasPointerEvents) {
setPointerEvent(hasPointerEvents);
@@ -25,7 +25,7 @@ function initializeModules(hasPointerEvents) {
React = require('react');
ReactDOM = require('react-dom');
FocusResponder = require('react-events/focus').FocusResponder;
useFocusResponder = require('react-events/focus').useFocusResponder;
useFocus = require('react-events/focus').useFocus;
}
const forcePointerEvents = true;
@@ -54,7 +54,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
onFocus = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useFocusResponder({
const listener = useFocus({
disabled: true,
onBlur,
onFocus,
@@ -80,7 +80,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
onBlur = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useFocusResponder({
const listener = useFocus({
onBlur,
});
return <div ref={ref} listeners={listener} />;
@@ -104,7 +104,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
ref = React.createRef();
innerRef = React.createRef();
const Component = () => {
const listener = useFocusResponder({
const listener = useFocus({
onFocus,
});
return (
@@ -203,7 +203,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
ref = React.createRef();
innerRef = React.createRef();
const Component = () => {
const listener = useFocusResponder({
const listener = useFocus({
onFocusChange,
});
return (
@@ -242,7 +242,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
ref = React.createRef();
innerRef = React.createRef();
const Component = () => {
const listener = useFocusResponder({
const listener = useFocus({
onFocusVisibleChange,
});
return (
@@ -315,7 +315,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
};
const Inner = () => {
const listener = useFocusResponder({
const listener = useFocus({
onBlur: createEventHandler('inner: onBlur'),
onFocus: createEventHandler('inner: onFocus'),
onFocusChange: createEventHandler('inner: onFocusChange'),
@@ -324,7 +324,7 @@ describe.each(table)('Focus responder', hasPointerEvents => {
};
const Outer = () => {
const listener = useFocusResponder({
const listener = useFocus({
onBlur: createEventHandler('outer: onBlur'),
onFocus: createEventHandler('outer: onFocus'),
onFocusChange: createEventHandler('outer: onFocusChange'),

View File

@@ -15,7 +15,7 @@ let React;
let ReactFeatureFlags;
let ReactDOM;
let FocusWithinResponder;
let useFocusWithinResponder;
let useFocusWithin;
const initializeModules = hasPointerEvents => {
setPointerEvent(hasPointerEvents);
@@ -25,8 +25,7 @@ const initializeModules = hasPointerEvents => {
React = require('react');
ReactDOM = require('react-dom');
FocusWithinResponder = require('react-events/focus').FocusWithinResponder;
useFocusWithinResponder = require('react-events/focus')
.useFocusWithinResponder;
useFocusWithin = require('react-events/focus').useFocusWithin;
};
const forcePointerEvents = true;
@@ -55,7 +54,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
onFocusWithinVisibleChange = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useFocusWithinResponder({
const listener = useFocusWithin({
disabled: true,
onFocusWithinChange,
onFocusWithinVisibleChange,
@@ -83,7 +82,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
innerRef = React.createRef();
innerRef2 = React.createRef();
const Component = () => {
const listener = useFocusWithinResponder({
const listener = useFocusWithin({
onFocusWithinChange,
});
return (
@@ -151,7 +150,7 @@ describe.each(table)('FocusWithin responder', hasPointerEvents => {
innerRef = React.createRef();
innerRef2 = React.createRef();
const Component = () => {
const listener = useFocusWithinResponder({
const listener = useFocusWithin({
onFocusWithinVisibleChange,
});
return (

View File

@@ -15,7 +15,7 @@ let React;
let ReactFeatureFlags;
let ReactDOM;
let HoverResponder;
let useHoverResponder;
let useHover;
function initializeModules(hasPointerEvents) {
jest.resetModules();
@@ -26,7 +26,7 @@ function initializeModules(hasPointerEvents) {
React = require('react');
ReactDOM = require('react-dom');
HoverResponder = require('react-events/hover').HoverResponder;
useHoverResponder = require('react-events/hover').useHoverResponder;
useHover = require('react-events/hover').useHover;
}
const forcePointerEvents = true;
@@ -57,7 +57,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
onHoverEnd = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useHoverResponder({
const listener = useHover({
disabled: true,
onHoverChange,
onHoverStart,
@@ -87,7 +87,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
onHoverStart = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useHoverResponder({
const listener = useHover({
onHoverStart: onHoverStart,
});
return <div ref={ref} listeners={listener} />;
@@ -124,7 +124,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
onHoverChange = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useHoverResponder({
const listener = useHover({
onHoverChange,
});
return <div ref={ref} listeners={listener} />;
@@ -157,7 +157,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
onHoverEnd = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useHoverResponder({
const listener = useHover({
onHoverEnd,
});
return <div ref={ref} listeners={listener} />;
@@ -201,7 +201,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
const onHoverMove = jest.fn();
const ref = React.createRef();
const Component = () => {
const listener = useHoverResponder({
const listener = useHover({
onHoverMove,
});
return <div ref={ref} listeners={listener} />;
@@ -229,7 +229,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
};
const Inner = () => {
const listener = useHoverResponder({
const listener = useHover({
onHoverStart: createEventHandler('inner: onHoverStart'),
onHoverEnd: createEventHandler('inner: onHoverEnd'),
onHoverChange: createEventHandler('inner: onHoverChange'),
@@ -238,7 +238,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
};
const Outer = () => {
const listener = useHoverResponder({
const listener = useHover({
onHoverStart: createEventHandler('outer: onHoverStart'),
onHoverEnd: createEventHandler('outer: onHoverEnd'),
onHoverChange: createEventHandler('outer: onHoverChange'),
@@ -305,7 +305,7 @@ describe.each(table)('Hover responder', hasPointerEvents => {
eventLog.push(propertiesWeCareAbout);
};
const Component = () => {
const listener = useHoverResponder({
const listener = useHover({
onHoverStart: logEvent,
onHoverEnd: logEvent,
onHoverMove: logEvent,

View File

@@ -13,8 +13,8 @@ let React;
let ReactFeatureFlags;
let ReactDOM;
let InputResponder;
let useInputResponder;
let usePressResponder;
let useInput;
let usePress;
let Scheduler;
const setUntrackedChecked = Object.getOwnPropertyDescriptor(
@@ -41,8 +41,8 @@ const modulesInit = () => {
ReactDOM = require('react-dom');
Scheduler = require('scheduler');
InputResponder = require('react-events/input').InputResponder;
useInputResponder = require('react-events/input').useInputResponder;
usePressResponder = require('react-events/press').usePressResponder;
useInput = require('react-events/input').useInput;
usePress = require('react-events/press').usePress;
};
describe('Input event responder', () => {
@@ -70,7 +70,7 @@ describe('Input event responder', () => {
ref = React.createRef();
function Component() {
const listener = useInputResponder({
const listener = useInput({
disabled: true,
onChange,
onValueChange,
@@ -117,7 +117,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -157,7 +157,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -200,7 +200,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -242,7 +242,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -283,7 +283,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -345,7 +345,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -385,7 +385,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -438,7 +438,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -483,7 +483,7 @@ describe('Input event responder', () => {
}
function Radio1() {
const listener = useInputResponder({
const listener = useInput({
onChange: onChange1,
onValueChange: onValueChange1,
});
@@ -491,7 +491,7 @@ describe('Input event responder', () => {
}
function Radio2() {
const listener = useInputResponder({
const listener = useInput({
onChange: onChange2,
onValueChange: onValueChange2,
});
@@ -558,7 +558,7 @@ describe('Input event responder', () => {
onChangeCalled = 0;
onValueChangeCalled = 0;
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -586,7 +586,7 @@ describe('Input event responder', () => {
onChangeCalled = 0;
onValueChangeCalled = 0;
function Component2() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -612,7 +612,7 @@ describe('Input event responder', () => {
onChangeCalled = 0;
onValueChangeCalled = 0;
function Component3() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -652,7 +652,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -689,7 +689,7 @@ describe('Input event responder', () => {
}
function Component() {
const listener = useInputResponder({
const listener = useInput({
onChange,
onValueChange,
});
@@ -761,7 +761,7 @@ describe('Input event responder', () => {
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInputResponder({
const listener = useInput({
onChange,
});
return (
@@ -821,7 +821,7 @@ describe('Input event responder', () => {
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInputResponder({
const listener = useInput({
onChange,
});
return (
@@ -896,7 +896,7 @@ describe('Input event responder', () => {
let ops = [];
function Component({innerRef, onChange, controlledValue}) {
const listener = useInputResponder({
const listener = useInput({
onChange,
});
return (
@@ -961,7 +961,7 @@ describe('Input event responder', () => {
controlledValue,
pressListener,
}) {
const inputListener = useInputResponder({
const inputListener = useInput({
onChange,
});
return (
@@ -975,7 +975,7 @@ describe('Input event responder', () => {
}
function PressWrapper({innerRef, onPress, onChange, controlledValue}) {
const pressListener = usePressResponder({
const pressListener = usePress({
onPress,
});
return (

View File

@@ -12,7 +12,7 @@
let React;
let ReactFeatureFlags;
let ReactDOM;
let useKeyboardResponder;
let useKeyboard;
import {createEventTarget} from '../testing-library';
@@ -22,7 +22,7 @@ function initializeModules(hasPointerEvents) {
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
useKeyboardResponder = require('react-events/keyboard').useKeyboardResponder;
useKeyboard = require('react-events/keyboard').useKeyboard;
}
describe('Keyboard event responder', () => {
@@ -48,7 +48,7 @@ describe('Keyboard event responder', () => {
onKeyUp = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useKeyboardResponder({
const listener = useKeyboard({
disabled: true,
onKeyDown,
onKeyUp,
@@ -74,7 +74,7 @@ describe('Keyboard event responder', () => {
onKeyDown = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useKeyboardResponder({
const listener = useKeyboard({
onKeyDown,
});
return <div ref={ref} listeners={listener} />;
@@ -100,7 +100,7 @@ describe('Keyboard event responder', () => {
onKeyUp = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useKeyboardResponder({
const listener = useKeyboard({
onKeyDown,
onKeyUp,
});

View File

@@ -9,13 +9,17 @@
'use strict';
import {createEventTarget, setPointerEvent} from '../testing-library';
import {
buttonsType,
createEventTarget,
setPointerEvent,
} from '../testing-library';
let React;
let ReactFeatureFlags;
let ReactDOM;
let PressResponder;
let usePressResponder;
let usePress;
function initializeModules(hasPointerEvents) {
jest.resetModules();
@@ -25,7 +29,7 @@ function initializeModules(hasPointerEvents) {
React = require('react');
ReactDOM = require('react-dom');
PressResponder = require('react-events/press').PressResponder;
usePressResponder = require('react-events/press').usePressResponder;
usePress = require('react-events/press').usePress;
}
function removePressMoveStrings(eventString) {
@@ -64,7 +68,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
onPressEnd = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
disabled: true,
onPressStart,
onPress,
@@ -93,7 +97,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
onPressStart = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPressStart,
});
return <div ref={ref} listeners={listener} />;
@@ -114,36 +118,36 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
},
);
it('is called after auxillary-button pointer down', () => {
it('is called after middle-button pointer down', () => {
const target = createEventTarget(ref.current);
target.pointerdown({button: 1, pointerType: 'mouse'});
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
expect(onPressStart).toHaveBeenCalledTimes(1);
expect(onPressStart).toHaveBeenCalledWith(
expect.objectContaining({
button: 'auxillary',
buttons: buttonsType.middle,
pointerType: 'mouse',
type: 'pressstart',
}),
);
});
it('is not called after pointer move following auxillary-button press', () => {
it('is not called after pointer move following middle-button press', () => {
const node = ref.current;
const target = createEventTarget(node);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
target.pointerdown({button: 1, pointerType: 'mouse'});
target.pointerup({button: 1, pointerType: 'mouse'});
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
target.pointerup({pointerType: 'mouse'});
target.pointerhover({x: 110, y: 110});
target.pointerhover({x: 50, y: 50});
expect(onPressStart).toHaveBeenCalledTimes(1);
});
it('ignores any events not caused by primary/auxillary-click or touch/pen contact', () => {
it('ignores any events not caused by primary/middle-click or touch/pen contact', () => {
const target = createEventTarget(ref.current);
target.pointerdown({button: 2});
target.pointerup({button: 2});
target.pointerdown({button: 5});
target.pointerup({button: 5});
target.pointerdown({buttons: buttonsType.secondary});
target.pointerup({buttons: buttonsType.secondary});
target.pointerdown({buttons: buttonsType.eraser});
target.pointerup({buttons: buttonsType.eraser});
expect(onPressStart).toHaveBeenCalledTimes(0);
});
@@ -187,7 +191,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
onPressEnd = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPressEnd,
});
return <div ref={ref} listeners={listener} />;
@@ -209,14 +213,14 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
},
);
it('is called after auxillary-button pointer up', () => {
it('is called after middle-button pointer up', () => {
const target = createEventTarget(ref.current);
target.pointerdown({button: 1, pointerType: 'mouse'});
target.pointerup({button: 1, pointerType: 'mouse'});
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
target.pointerup({pointerType: 'mouse'});
expect(onPressEnd).toHaveBeenCalledTimes(1);
expect(onPressEnd).toHaveBeenCalledWith(
expect.objectContaining({
button: 'auxillary',
buttons: buttonsType.middle,
pointerType: 'mouse',
type: 'pressend',
}),
@@ -282,7 +286,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
onPressChange = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPressChange,
});
return <div ref={ref} listeners={listener} />;
@@ -322,7 +326,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
onPress = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPress,
});
return <div ref={ref} listeners={listener} />;
@@ -350,10 +354,10 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
},
);
it('is not called after auxillary-button press', () => {
it('is not called after middle-button press', () => {
const target = createEventTarget(ref.current);
target.pointerdown({button: 1, pointerType: 'mouse'});
target.pointerup({button: 1, pointerType: 'mouse'});
target.pointerdown({buttons: buttonsType.middle, pointerType: 'mouse'});
target.pointerup({pointerType: 'mouse'});
expect(onPress).not.toHaveBeenCalled();
});
@@ -370,7 +374,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
it('is not called after invalid "keyup" event', () => {
const inputRef = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return <input ref={inputRef} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -400,7 +404,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const divRef = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return (
<div ref={divRef} listeners={listener}>
<button ref={buttonRef} />
@@ -425,7 +429,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
onPressMove = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPressMove,
});
return <div ref={ref} listeners={listener} />;
@@ -460,7 +464,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const target = createEventTarget(ref.current);
target.setBoundingClientRect({x: 0, y: 0, width: 100, height: 100});
target.keydown({key: 'Enter'});
target.pointermove({button: -1, pointerType: 'mouse', x: 10, y: 10});
target.pointermove({
buttons: buttonsType.none,
pointerType: 'mouse',
x: 10,
y: 10,
});
expect(onPressMove).not.toBeCalled();
});
});
@@ -476,7 +485,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
events.push(msg);
};
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPress: createEventHandler('onPress'),
onPressChange: createEventHandler('onPressChange'),
onPressMove: createEventHandler('onPressMove'),
@@ -566,7 +575,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const pressRetentionOffset = {top: 40, bottom: 40, left: 40, right: 40};
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPress: createEventHandler('onPress'),
onPressChange: createEventHandler('onPressChange'),
onPressMove: createEventHandler('onPressMove'),
@@ -710,7 +719,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
const Inner = () => {
const listener = usePressResponder({
const listener = usePress({
onPress: createEventHandler('inner: onPress'),
onPressChange: createEventHandler('inner: onPressChange'),
onPressMove: createEventHandler('inner: onPressMove'),
@@ -731,7 +740,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
const Outer = () => {
const listener = usePressResponder({
const listener = usePress({
onPress: createEventHandler('outer: onPress'),
onPressChange: createEventHandler('outer: onPressChange'),
onPressMove: createEventHandler('outer: onPressMove'),
@@ -768,12 +777,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const onPress = jest.fn();
const Inner = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return <div ref={ref} listeners={listener} />;
};
const Outer = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return (
<div listeners={listener}>
<Inner />
@@ -795,12 +804,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const onPressEnd = jest.fn();
const Inner = () => {
const listener = usePressResponder({onPressStart, onPressEnd});
const listener = usePress({onPressStart, onPressEnd});
return <div ref={ref} listeners={listener} />;
};
const Outer = () => {
const listener = usePressResponder({onPressStart, onPressEnd});
const listener = usePress({onPressStart, onPressEnd});
return (
<div listeners={listener}>
<Inner />
@@ -823,12 +832,12 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const onPressChange = jest.fn();
const Inner = () => {
const listener = usePressResponder({onPressChange});
const listener = usePress({onPressChange});
return <div ref={ref} listeners={listener} />;
};
const Outer = () => {
const listener = usePressResponder({onPressChange});
const listener = usePress({onPressChange});
return (
<div listeners={listener}>
<Inner />
@@ -853,7 +862,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -873,7 +882,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -894,7 +903,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const buttonRef = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return (
<a href="#">
<button ref={buttonRef} listeners={listener} />
@@ -915,7 +924,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return (
<a href="#" listeners={listener}>
<div ref={ref} />
@@ -939,7 +948,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress});
const listener = usePress({onPress});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -961,7 +970,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress, preventDefault: false});
const listener = usePress({onPress, preventDefault: false});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -981,7 +990,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPress, preventDefault: false});
const listener = usePress({onPress, preventDefault: false});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -1003,7 +1012,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPressEnd});
const listener = usePress({onPressEnd});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -1020,7 +1029,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder({onPressEnd});
const listener = usePress({onPressEnd});
return <a href="#" ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -1038,7 +1047,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const containerRef = React.createRef();
const Component = () => {
const listener = usePressResponder({onPressEnd});
const listener = usePress({onPressEnd});
return (
<div ref={containerRef}>
<a ref={ref} listeners={listener} />
@@ -1060,7 +1069,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const outsideRef = React.createRef();
const Component = () => {
const listener = usePressResponder({onPressEnd});
const listener = usePress({onPressEnd});
return (
<div>
<a ref={ref} listeners={listener} />
@@ -1085,7 +1094,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const ref = React.createRef();
const Component = () => {
const listener = usePressResponder();
const listener = usePress();
return <button ref={ref} listeners={listener} />;
};
ReactDOM.render(<Component />, container);
@@ -1119,7 +1128,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
};
const Component = () => {
const listener = usePressResponder({
const listener = usePress({
onPressStart: logEvent,
onPressEnd: logEvent,
onPressMove: logEvent,
@@ -1239,7 +1248,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
updateCounter(count => count + 1);
}
const listener = usePressResponder({
const listener = usePress({
onPress: handlePress,
});
@@ -1307,7 +1316,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
updateCounter(count => count + 1);
}
const listener = usePressResponder({
const listener = usePress({
onPress: handlePress,
});
@@ -1389,7 +1398,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
updatePressesCount(pressesCount + 1);
}
const listener = usePressResponder({
const listener = usePress({
onPress: handlePress,
});
@@ -1430,7 +1439,7 @@ describe.each(environmentTable)('Press responder', hasPointerEvents => {
const pointerDownEvent = jest.fn();
const Component = () => {
const listener = usePressResponder({stopPropagation: true});
const listener = usePress({stopPropagation: true});
return <div ref={ref} listeners={listener} />;
};

View File

@@ -14,7 +14,7 @@ import {createEventTarget, setPointerEvent} from '../testing-library';
let React;
let ReactFeatureFlags;
let ReactDOM;
let useScrollResponder;
let useScroll;
const forcePointerEvents = true;
const table = [[forcePointerEvents], [!forcePointerEvents]];
@@ -26,7 +26,7 @@ const initializeModules = hasPointerEvents => {
ReactFeatureFlags.enableFlareAPI = true;
React = require('react');
ReactDOM = require('react-dom');
useScrollResponder = require('react-events/scroll').useScrollResponder;
useScroll = require('react-events/scroll').useScroll;
};
describe.each(table)('Scroll responder', hasPointerEvents => {
@@ -51,7 +51,7 @@ describe.each(table)('Scroll responder', hasPointerEvents => {
onScroll = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useScrollResponder({
const listener = useScroll({
disabled: true,
onScroll,
});
@@ -74,7 +74,7 @@ describe.each(table)('Scroll responder', hasPointerEvents => {
onScroll = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useScrollResponder({
const listener = useScroll({
onScroll,
});
return <div ref={ref} listeners={listener} />;
@@ -145,7 +145,7 @@ describe.each(table)('Scroll responder', hasPointerEvents => {
onScrollDragStart = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useScrollResponder({
const listener = useScroll({
onScrollDragStart,
});
return <div ref={ref} listeners={listener} />;
@@ -175,7 +175,7 @@ describe.each(table)('Scroll responder', hasPointerEvents => {
onScrollDragEnd = jest.fn();
ref = React.createRef();
const Component = () => {
const listener = useScrollResponder({
const listener = useScroll({
onScrollDragEnd,
});
return <div ref={ref} listeners={listener} />;

View File

@@ -8,6 +8,7 @@
*/
'use strict';
/**
* Change environment support for PointerEvent.
*/
@@ -49,3 +50,26 @@ export const platform = {
}
},
};
/**
* Buttons bitmask
*/
export const buttonsType = {
none: 0,
// left-mouse
// touch contact
// pen contact
primary: 1,
// right-mouse
// pen barrel button
secondary: 2,
// middle mouse
middle: 4,
// back mouse
back: 8,
// forward mouse
forward: 16,
// pen eraser
eraser: 32,
};

View File

@@ -10,7 +10,7 @@
'use strict';
import * as domEvents from './domEvents';
import {hasPointerEvent, platform} from './domEnvironment';
import {buttonsType, hasPointerEvent, platform} from './domEnvironment';
function emptyFunction() {}
@@ -29,30 +29,33 @@ export function contextmenu(
) {
const dispatch = arg => target.dispatchEvent(arg);
if (pointerType === 'touch') {
const button = 0;
if (hasPointerEvent()) {
dispatch(domEvents.pointerdown({button, pointerType}));
dispatch(
domEvents.pointerdown({buttons: buttonsType.primary, pointerType}),
);
}
dispatch(domEvents.touchstart());
dispatch(domEvents.contextmenu({button, preventDefault}));
dispatch(
domEvents.contextmenu({buttons: buttonsType.none, preventDefault}),
);
} else if (pointerType === 'mouse') {
if (modified === true) {
const button = 0;
const buttons = buttonsType.primary;
const ctrlKey = true;
if (hasPointerEvent()) {
dispatch(domEvents.pointerdown({button, ctrlKey, pointerType}));
dispatch(domEvents.pointerdown({buttons, ctrlKey, pointerType}));
}
dispatch(domEvents.mousedown({button, ctrlKey}));
dispatch(domEvents.mousedown({buttons, ctrlKey}));
if (platform.get() === 'mac') {
dispatch(domEvents.contextmenu({button, ctrlKey, preventDefault}));
dispatch(domEvents.contextmenu({buttons, ctrlKey, preventDefault}));
}
} else {
const button = 2;
const buttons = buttonsType.secondary;
if (hasPointerEvent()) {
dispatch(domEvents.pointerdown({button, pointerType}));
dispatch(domEvents.pointerdown({buttons, pointerType}));
}
dispatch(domEvents.mousedown({button}));
dispatch(domEvents.contextmenu({button, preventDefault}));
dispatch(domEvents.mousedown({buttons}));
dispatch(domEvents.contextmenu({buttons, preventDefault}));
}
}
}
@@ -74,7 +77,7 @@ export function pointercancel(target, payload) {
export function pointerdown(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const pointerType = getPointerType(defaultPayload);
const payload = {button: 0, buttons: 2, ...defaultPayload};
const payload = {buttons: buttonsType.primary, ...defaultPayload};
if (pointerType === 'mouse') {
if (hasPointerEvent()) {
@@ -147,7 +150,7 @@ export function pointermove(target, payload) {
export function pointerup(target, defaultPayload) {
const dispatch = arg => target.dispatchEvent(arg);
const pointerType = getPointerType(defaultPayload);
const payload = {button: 0, buttons: 2, ...defaultPayload};
const payload = {buttons: buttonsType.none, ...defaultPayload};
if (pointerType === 'mouse') {
if (hasPointerEvent()) {

View File

@@ -9,6 +9,8 @@
'use strict';
import {buttonsType} from './domEnvironment';
/**
* Native event object mocks for higher-level events.
*
@@ -24,6 +26,9 @@
* 3. PointerEvent and TouchEvent fields are normalized (e.g., 'rotationAngle' -> 'twist')
*/
const defaultPointerSize = 23;
const defaultBrowserChromeSize = 50;
function emptyFunction() {}
function createEvent(type, data = {}) {
@@ -57,8 +62,7 @@ function createPointerEvent(
type,
{
altKey = false,
button = -1,
buttons = 0,
buttons = buttonsType.none,
ctrlKey = false,
height,
metaKey = false,
@@ -86,7 +90,6 @@ function createPointerEvent(
return createEvent(type, {
altKey,
button,
buttons,
clientX: x,
clientY: y,
@@ -94,7 +97,12 @@ function createPointerEvent(
getModifierState(keyArg) {
createGetModifierState(keyArg, modifierState);
},
height: pointerType === 'mouse' ? 1 : height != null ? height : 11.5,
height:
pointerType === 'mouse'
? 1
: height != null
? height
: defaultPointerSize,
metaKey,
movementX,
movementY,
@@ -107,13 +115,14 @@ function createPointerEvent(
pressure,
preventDefault,
screenX: x,
screenY: y + 50, // arbitrary value to emulate browser chrome, etc
screenY: y + defaultBrowserChromeSize,
shiftKey,
tangentialPressure,
tiltX,
tiltY,
twist,
width: pointerType === 'mouse' ? 1 : width != null ? width : 11.5,
width:
pointerType === 'mouse' ? 1 : width != null ? width : defaultPointerSize,
});
}
@@ -147,8 +156,7 @@ function createMouseEvent(
type,
{
altKey = false,
button = -1,
buttons = 0,
buttons = buttonsType.none,
ctrlKey = false,
metaKey = false,
movementX = 0,
@@ -167,7 +175,6 @@ function createMouseEvent(
return createEvent(type, {
altKey,
button,
buttons,
clientX: x,
clientY: y,
@@ -184,7 +191,7 @@ function createMouseEvent(
pageY: pageY || y,
preventDefault,
screenX: x,
screenY: y + 50, // arbitrary value to emulate browser chrome, etc
screenY: y + defaultBrowserChromeSize,
shiftKey,
});
}
@@ -194,7 +201,7 @@ function createTouchEvent(
{
altKey = false,
ctrlKey = false,
height = 11.5,
height = defaultPointerSize,
metaKey = false,
pageX,
pageY,
@@ -202,7 +209,7 @@ function createTouchEvent(
preventDefault = emptyFunction,
shiftKey = false,
twist = 0,
width = 11.5,
width = defaultPointerSize,
x = 0,
y = 0,
} = {},
@@ -214,13 +221,15 @@ function createTouchEvent(
identifier: pointerId,
pageX: pageX || x,
pageY: pageY || y,
radiusX: width,
radiusY: height,
radiusX: width / 2,
radiusY: height / 2,
rotationAngle: twist,
screenX: x,
screenY: y + 50, // arbitrary value to emulate browser chrome, etc
screenY: y + defaultBrowserChromeSize,
};
const activeTouch = type !== 'touchend' ? [touch] : null;
return createEvent(type, {
altKey,
changedTouches: [touch],
@@ -228,8 +237,8 @@ function createTouchEvent(
metaKey,
preventDefault,
shiftKey,
targetTouches: type !== 'touchend' ? [touch] : null,
touches: [touch],
targetTouches: activeTouch,
touches: activeTouch,
});
}
@@ -290,7 +299,12 @@ export function pointercancel(payload) {
}
export function pointerdown(payload) {
return createPointerEvent('pointerdown', {button: 0, buttons: 2, ...payload});
const isTouch = payload != null && payload.pointerType === 'mouse';
return createPointerEvent('pointerdown', {
buttons: buttonsType.primary,
pressure: isTouch ? 1 : 0.5,
...payload,
});
}
export function pointerenter(payload) {
@@ -314,7 +328,10 @@ export function pointerover(payload) {
}
export function pointerup(payload) {
return createPointerEvent('pointerup', {button: 0, buttons: 2, ...payload});
return createPointerEvent('pointerup', {
...payload,
buttons: buttonsType.none,
});
}
/**
@@ -322,7 +339,10 @@ export function pointerup(payload) {
*/
export function mousedown(payload) {
return createMouseEvent('mousedown', {button: 0, buttons: 2, ...payload});
return createMouseEvent('mousedown', {
buttons: buttonsType.primary,
...payload,
});
}
export function mouseenter(payload) {
@@ -346,7 +366,10 @@ export function mouseover(payload) {
}
export function mouseup(payload) {
return createMouseEvent('mouseup', {button: 0, buttons: 2, ...payload});
return createMouseEvent('mouseup', {
...payload,
buttons: buttonsType.none,
});
}
/**

View File

@@ -11,7 +11,12 @@
import * as domEvents from './domEvents';
import * as domEventSequences from './domEventSequences';
import {hasPointerEvent, setPointerEvent, platform} from './domEnvironment';
import {
buttonsType,
hasPointerEvent,
setPointerEvent,
platform,
} from './domEnvironment';
const createEventTarget = node => ({
node,
@@ -101,4 +106,10 @@ const createEventTarget = node => ({
},
});
export {createEventTarget, platform, hasPointerEvent, setPointerEvent};
export {
buttonsType,
createEventTarget,
platform,
hasPointerEvent,
setPointerEvent,
};

View File

@@ -533,7 +533,7 @@ export const PressResponder = React.unstable_createResponder(
pressResponderImpl,
);
export function usePressResponder(
export function usePress(
props: PressProps,
): ReactEventResponderListener<any, any> {
return React.unstable_useResponder(PressResponder, props);

View File

@@ -449,7 +449,6 @@ export function mountResponderInstance(
props: Object,
state: Object,
instance: Instance,
rootContainerInstance: Container,
) {
if (enableFlareAPI) {
const {rootEventTypes} = responder;

View File

@@ -501,7 +501,6 @@ export function mountResponderInstance(
props: Object,
state: Object,
instance: Instance,
rootContainerInstance: Container,
) {
throw new Error('Not yet implemented.');
}

View File

@@ -46,6 +46,7 @@ import {
NoEffect,
PerformedWork,
Placement,
Hydrating,
ContentReset,
DidCapture,
Update,
@@ -944,11 +945,10 @@ function updateHostRoot(current, workInProgress, renderExpirationTime) {
// be any children to hydrate which is effectively the same thing as
// not hydrating.
// This is a bit of a hack. We track the host root as a placement to
// know that we're currently in a mounting state. That way isMounted
// works as expected. We must reset this before committing.
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag |= Placement;
// Mark the host root with a Hydrating effect to know that we're
// currently in a mounting state. That way isMounted, findDOMNode and
// event replaying works as expected.
workInProgress.effectTag |= Hydrating;
// Ensure that children mount into this root without tracking
// side-effects. This ensures that we don't store Placement effects on
@@ -2095,12 +2095,24 @@ function updateDehydratedSuspenseComponent(
);
const nextProps = workInProgress.pendingProps;
const nextChildren = nextProps.children;
workInProgress.child = mountChildFibers(
const child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderExpirationTime,
);
let node = child;
while (node) {
// Mark each child as hydrating. This is a fast path to know whether this
// tree is part of a hydrating tree. This is used to determine if a child
// node has fully mounted yet, and for scheduling event replaying.
// Conceptually this is similar to Placement in that a new subtree is
// inserted into the React tree here. It just happens to not need DOM
// mutations because it already exists.
node.effectTag |= Hydrating;
node = node.sibling;
}
workInProgress.child = child;
return workInProgress.child;
}
}

View File

@@ -118,6 +118,7 @@ import {
} from './ReactHookEffectTags';
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork';
import {runWithPriority, NormalPriority} from './SchedulerWithReactIntegration';
import {updateEventListeners} from './ReactFiberEvents';
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
if (__DEV__) {
@@ -1331,6 +1332,13 @@ function commitWork(current: Fiber | null, finishedWork: Fiber): void {
finishedWork,
);
}
if (enableFlareAPI) {
const prevListeners = oldProps.listeners;
const nextListeners = newProps.listeners;
if (prevListeners !== nextListeners) {
updateEventListeners(nextListeners, instance, finishedWork);
}
}
}
return;
}

View File

@@ -9,12 +9,7 @@
import type {Fiber} from './ReactFiber';
import type {ExpirationTime} from './ReactFiberExpirationTime';
import type {
ReactEventResponder,
ReactEventResponderInstance,
ReactFundamentalComponentInstance,
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {ReactFundamentalComponentInstance} from 'shared/ReactTypes';
import type {FiberRoot} from './ReactFiberRoot';
import type {
Instance,
@@ -31,7 +26,6 @@ import type {SuspenseContext} from './ReactFiberSuspenseContext';
import {now} from './SchedulerWithReactIntegration';
import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols';
import {
IndeterminateComponent,
FunctionComponent,
@@ -56,7 +50,6 @@ import {
} from 'shared/ReactWorkTags';
import {NoMode, BatchedMode} from './ReactTypeOfMode';
import {
Placement,
Ref,
Update,
NoEffect,
@@ -79,8 +72,6 @@ import {
createContainerChildSet,
appendChildToContainerChildSet,
finalizeContainerChildren,
mountResponderInstance,
unmountResponderInstance,
getFundamentalComponentInstance,
mountFundamentalComponent,
cloneFundamentalInstance,
@@ -92,8 +83,6 @@ import {
getHostContext,
popHostContainer,
} from './ReactFiberHostContext';
import {NoWork} from './ReactFiberExpirationTime';
import {createResponderInstance} from './ReactFiberEvents';
import {
suspenseStackCursor,
InvisibleParentSuspenseContext,
@@ -133,10 +122,7 @@ import {
import {createFundamentalStateInstance} from './ReactFiberFundamental';
import {Never} from './ReactFiberExpirationTime';
import {resetChildFibers} from './ReactChildFiber';
import warning from 'shared/warning';
const emptyObject = {};
const isArray = Array.isArray;
import {updateEventListeners} from './ReactFiberEvents';
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
@@ -670,9 +656,6 @@ function completeWork(
// If we hydrated, pop so that we can delete any remaining children
// that weren't hydrated.
popHydrationState(workInProgress);
// This resets the hacky state to fix isMounted before committing.
// TODO: Delete this when we delete isMounted and findDOMNode.
workInProgress.effectTag &= ~Placement;
}
updateHostContainer(workInProgress);
break;
@@ -693,14 +676,8 @@ function completeWork(
if (enableFlareAPI) {
const prevListeners = current.memoizedProps.listeners;
const nextListeners = newProps.listeners;
const instance = workInProgress.stateNode;
if (prevListeners !== nextListeners) {
updateEventListeners(
nextListeners,
instance,
rootContainerInstance,
workInProgress,
);
markUpdate(workInProgress);
}
}
@@ -742,12 +719,7 @@ function completeWork(
const instance = workInProgress.stateNode;
const listeners = newProps.listeners;
if (listeners != null) {
updateEventListeners(
listeners,
instance,
rootContainerInstance,
workInProgress,
);
updateEventListeners(listeners, instance, workInProgress);
}
}
} else {
@@ -764,12 +736,7 @@ function completeWork(
if (enableFlareAPI) {
const listeners = newProps.listeners;
if (listeners != null) {
updateEventListeners(
listeners,
instance,
rootContainerInstance,
workInProgress,
);
updateEventListeners(listeners, instance, workInProgress);
}
}
@@ -859,14 +826,13 @@ function completeWork(
if ((workInProgress.effectTag & DidCapture) === NoEffect) {
// This boundary did not suspend so it's now hydrated and unsuspended.
workInProgress.memoizedState = null;
if (enableSuspenseCallback) {
// Notify the callback.
workInProgress.effectTag |= Update;
}
} else {
// Something suspended. Schedule an effect to attach retry listeners.
workInProgress.effectTag |= Update;
}
// If nothing suspended, we need to schedule an effect to mark this boundary
// as having hydrated so events know that they're free be invoked.
// It's also a signal to replay events and the suspense callback.
// If something suspended, schedule an effect to attach retry listeners.
// So we might as well always mark this.
workInProgress.effectTag |= Update;
return null;
}
}
@@ -1258,156 +1224,4 @@ function completeWork(
return null;
}
function mountEventResponder(
responder: ReactEventResponder<any, any>,
responderProps: Object,
instance: Instance,
rootContainerInstance: Container,
fiber: Fiber,
respondersMap: Map<
ReactEventResponder<any, any>,
ReactEventResponderInstance<any, any>,
>,
) {
let responderState = emptyObject;
const getInitialState = responder.getInitialState;
if (getInitialState !== null) {
responderState = getInitialState(responderProps);
}
const responderInstance = createResponderInstance(
responder,
responderProps,
responderState,
instance,
fiber,
);
mountResponderInstance(
responder,
responderInstance,
responderProps,
responderState,
instance,
rootContainerInstance,
);
respondersMap.set(responder, responderInstance);
}
function updateEventListener(
listener: ReactEventResponderListener<any, any>,
fiber: Fiber,
visistedResponders: Set<ReactEventResponder<any, any>>,
respondersMap: Map<
ReactEventResponder<any, any>,
ReactEventResponderInstance<any, any>,
>,
instance: Instance,
rootContainerInstance: Container,
): void {
let responder;
let props;
if (listener) {
responder = listener.responder;
props = listener.props;
}
invariant(
responder && responder.$$typeof === REACT_RESPONDER_TYPE,
'An invalid value was used as an event listener. Expect one or many event ' +
'listeners created via React.unstable_useResponder().',
);
const listenerProps = ((props: any): Object);
if (visistedResponders.has(responder)) {
// show warning
if (__DEV__) {
warning(
false,
'Duplicate event responder "%s" found in event listeners. ' +
'Event listeners passed to elements cannot use the same event responder more than once.',
responder.displayName,
);
}
return;
}
visistedResponders.add(responder);
const responderInstance = respondersMap.get(responder);
if (responderInstance === undefined) {
// Mount
mountEventResponder(
responder,
listenerProps,
instance,
rootContainerInstance,
fiber,
respondersMap,
);
} else {
// Update
responderInstance.props = listenerProps;
responderInstance.fiber = fiber;
}
}
function updateEventListeners(
listeners: any,
instance: Instance,
rootContainerInstance: Container,
fiber: Fiber,
): void {
const visistedResponders = new Set();
let dependencies = fiber.dependencies;
if (listeners != null) {
if (dependencies === null) {
dependencies = fiber.dependencies = {
expirationTime: NoWork,
firstContext: null,
responders: new Map(),
};
}
let respondersMap = dependencies.responders;
if (respondersMap === null) {
respondersMap = new Map();
}
if (isArray(listeners)) {
for (let i = 0, length = listeners.length; i < length; i++) {
const listener = listeners[i];
updateEventListener(
listener,
fiber,
visistedResponders,
respondersMap,
instance,
rootContainerInstance,
);
}
} else {
updateEventListener(
listeners,
fiber,
visistedResponders,
respondersMap,
instance,
rootContainerInstance,
);
}
}
if (dependencies !== null) {
const respondersMap = dependencies.responders;
if (respondersMap !== null) {
// Unmount
const mountedResponders = Array.from(respondersMap.keys());
for (let i = 0, length = mountedResponders.length; i < length; i++) {
const mountedResponder = mountedResponders[i];
if (!visistedResponders.has(mountedResponder)) {
const responderInstance = ((respondersMap.get(
mountedResponder,
): any): ReactEventResponderInstance<any, any>);
unmountResponderInstance(responderInstance);
respondersMap.delete(mountedResponder);
}
}
}
}
}
export {completeWork};

View File

@@ -8,59 +8,26 @@
*/
import type {Fiber} from './ReactFiber';
import type {Instance} from './ReactFiberHostConfig';
import type {
ReactEventResponder,
ReactEventResponderInstance,
ReactEventResponderListener,
} from 'shared/ReactTypes';
import type {Instance} from './ReactFiberHostConfig';
import {SuspenseComponent, Fragment} from 'shared/ReactWorkTags';
import {
mountResponderInstance,
unmountResponderInstance,
} from './ReactFiberHostConfig';
import {NoWork} from './ReactFiberExpirationTime';
export function createResponderListener(
responder: ReactEventResponder<any, any>,
props: Object,
): ReactEventResponderListener<any, any> {
const eventResponderListener = {
responder,
props,
};
if (__DEV__) {
Object.freeze(eventResponderListener);
}
return eventResponderListener;
}
import warning from 'shared/warning';
import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols';
export function isFiberSuspenseAndTimedOut(fiber: Fiber): boolean {
return fiber.tag === SuspenseComponent && fiber.memoizedState !== null;
}
import invariant from 'shared/invariant';
export function getSuspenseFallbackChild(fiber: Fiber): Fiber | null {
return ((((fiber.child: any): Fiber).sibling: any): Fiber).child;
}
export function isFiberSuspenseTimedOutChild(fiber: Fiber | null): boolean {
if (fiber === null) {
return false;
}
const parent = fiber.return;
if (parent !== null && parent.tag === Fragment) {
const grandParent = parent.return;
if (
grandParent !== null &&
grandParent.tag === SuspenseComponent &&
grandParent.stateNode !== null
) {
return true;
}
}
return false;
}
export function getSuspenseFiberFromTimedOutChild(fiber: Fiber): Fiber {
return ((((fiber.return: any): Fiber).return: any): Fiber);
}
const emptyObject = {};
const isArray = Array.isArray;
export function createResponderInstance(
responder: ReactEventResponder<any, any>,
@@ -78,3 +45,162 @@ export function createResponderInstance(
target,
};
}
function mountEventResponder(
responder: ReactEventResponder<any, any>,
responderProps: Object,
instance: Instance,
fiber: Fiber,
respondersMap: Map<
ReactEventResponder<any, any>,
ReactEventResponderInstance<any, any>,
>,
) {
let responderState = emptyObject;
const getInitialState = responder.getInitialState;
if (getInitialState !== null) {
responderState = getInitialState(responderProps);
}
const responderInstance = createResponderInstance(
responder,
responderProps,
responderState,
instance,
fiber,
);
mountResponderInstance(
responder,
responderInstance,
responderProps,
responderState,
instance,
);
respondersMap.set(responder, responderInstance);
}
function updateEventListener(
listener: ReactEventResponderListener<any, any>,
fiber: Fiber,
visistedResponders: Set<ReactEventResponder<any, any>>,
respondersMap: Map<
ReactEventResponder<any, any>,
ReactEventResponderInstance<any, any>,
>,
instance: Instance,
): void {
let responder;
let props;
if (listener) {
responder = listener.responder;
props = listener.props;
}
invariant(
responder && responder.$$typeof === REACT_RESPONDER_TYPE,
'An invalid value was used as an event listener. Expect one or many event ' +
'listeners created via React.unstable_useResponder().',
);
const listenerProps = ((props: any): Object);
if (visistedResponders.has(responder)) {
// show warning
if (__DEV__) {
warning(
false,
'Duplicate event responder "%s" found in event listeners. ' +
'Event listeners passed to elements cannot use the same event responder more than once.',
responder.displayName,
);
}
return;
}
visistedResponders.add(responder);
const responderInstance = respondersMap.get(responder);
if (responderInstance === undefined) {
// Mount (happens in either complete or commit phase)
mountEventResponder(
responder,
listenerProps,
instance,
fiber,
respondersMap,
);
} else {
// Update (happens during commit phase only)
responderInstance.props = listenerProps;
responderInstance.fiber = fiber;
}
}
export function updateEventListeners(
listeners: any,
instance: Instance,
fiber: Fiber,
): void {
const visistedResponders = new Set();
let dependencies = fiber.dependencies;
if (listeners != null) {
if (dependencies === null) {
dependencies = fiber.dependencies = {
expirationTime: NoWork,
firstContext: null,
responders: new Map(),
};
}
let respondersMap = dependencies.responders;
if (respondersMap === null) {
respondersMap = new Map();
}
if (isArray(listeners)) {
for (let i = 0, length = listeners.length; i < length; i++) {
const listener = listeners[i];
updateEventListener(
listener,
fiber,
visistedResponders,
respondersMap,
instance,
);
}
} else {
updateEventListener(
listeners,
fiber,
visistedResponders,
respondersMap,
instance,
);
}
}
if (dependencies !== null) {
const respondersMap = dependencies.responders;
if (respondersMap !== null) {
// Unmount
const mountedResponders = Array.from(respondersMap.keys());
for (let i = 0, length = mountedResponders.length; i < length; i++) {
const mountedResponder = mountedResponders[i];
if (!visistedResponders.has(mountedResponder)) {
const responderInstance = ((respondersMap.get(
mountedResponder,
): any): ReactEventResponderInstance<any, any>);
unmountResponderInstance(responderInstance);
respondersMap.delete(mountedResponder);
}
}
}
}
}
export function createResponderListener(
responder: ReactEventResponder<any, any>,
props: Object,
): ReactEventResponderListener<any, any> {
const eventResponderListener = {
responder,
props,
};
if (__DEV__) {
Object.freeze(eventResponderListener);
}
return eventResponderListener;
}

View File

@@ -1122,7 +1122,7 @@ function dispatchAction<S, A>(
if (__DEV__) {
warning(
arguments.length <= 3,
typeof arguments[3] !== 'function',
"State updates from the useState() and useReducer() Hooks don't support the " +
'second callback argument. To execute a side effect after ' +
'rendering, declare it in the component body with useEffect().',

View File

@@ -23,7 +23,7 @@ import {
HostText,
FundamentalComponent,
} from 'shared/ReactWorkTags';
import {NoEffect, Placement} from 'shared/ReactSideEffectTags';
import {NoEffect, Placement, Hydrating} from 'shared/ReactSideEffectTags';
import {enableFundamentalAPI} from 'shared/ReactFeatureFlags';
const ReactCurrentOwner = ReactSharedInternals.ReactCurrentOwner;
@@ -32,20 +32,21 @@ const MOUNTING = 1;
const MOUNTED = 2;
const UNMOUNTED = 3;
function isFiberMountedImpl(fiber: Fiber): number {
type MountState = 1 | 2 | 3;
function isFiberMountedImpl(fiber: Fiber): MountState {
let node = fiber;
if (!fiber.alternate) {
// If there is no alternate, this might be a new tree that isn't inserted
// yet. If it is, then it will have a pending insertion effect on it.
if ((node.effectTag & Placement) !== NoEffect) {
return MOUNTING;
}
while (node.return) {
node = node.return;
if ((node.effectTag & Placement) !== NoEffect) {
let nextNode = node;
do {
node = nextNode;
if ((node.effectTag & (Placement | Hydrating)) !== NoEffect) {
return MOUNTING;
}
}
nextNode = node.return;
} while (nextNode);
} else {
while (node.return) {
node = node.return;

View File

@@ -96,6 +96,8 @@ import {
Passive,
Incomplete,
HostEffectMask,
Hydrating,
HydratingAndUpdate,
} from 'shared/ReactSideEffectTags';
import {
NoWork,
@@ -1860,7 +1862,8 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
// updates, and deletions. To avoid needing to add a case for every possible
// bitmap value, we remove the secondary effects from the effect tag and
// switch on that value.
let primaryEffectTag = effectTag & (Placement | Update | Deletion);
let primaryEffectTag =
effectTag & (Placement | Update | Deletion | Hydrating);
switch (primaryEffectTag) {
case Placement: {
commitPlacement(nextEffect);
@@ -1883,6 +1886,18 @@ function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
commitWork(current, nextEffect);
break;
}
case Hydrating: {
nextEffect.effectTag &= ~Hydrating;
break;
}
case HydratingAndUpdate: {
nextEffect.effectTag &= ~Hydrating;
// Update
const current = nextEffect.alternate;
commitWork(current, nextEffect);
break;
}
case Update: {
const current = nextEffect.alternate;
commitWork(current, nextEffect);

View File

@@ -160,7 +160,9 @@ export function cancelCallback(callbackNode: mixed) {
export function flushSyncCallbackQueue() {
if (immediateQueueCallbackNode !== null) {
Scheduler_cancelCallback(immediateQueueCallbackNode);
const node = immediateQueueCallbackNode;
immediateQueueCallbackNode = null;
Scheduler_cancelCallback(node);
}
flushSyncCallbackQueueImpl();
}

View File

@@ -136,6 +136,7 @@ describe('ReactDebugFiberPerf', () => {
require('shared/ReactFeatureFlags').enableProfilerTimer = false;
require('shared/ReactFeatureFlags').replayFailedUnitOfWorkWithInvokeGuardedCallback = false;
require('shared/ReactFeatureFlags').debugRenderPhaseSideEffectsForStrictMode = false;
require('scheduler/src/SchedulerFeatureFlags').enableProfiling = false;
// Import after the polyfill is set up:
React = require('react');

View File

@@ -296,7 +296,6 @@ export function mountResponderInstance(
props: Object,
state: Object,
instance: Instance,
rootContainerInstance: Container,
) {
// noop
}

View File

@@ -144,5 +144,9 @@
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
get unstable_Profiling() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_Profiling;
},
});
});

View File

@@ -138,5 +138,9 @@
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
get unstable_Profiling() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_Profiling;
},
});
});

View File

@@ -138,5 +138,9 @@
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_UserBlockingPriority;
},
get unstable_Profiling() {
return global.React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED
.Scheduler.unstable_Profiling;
},
});
});

View File

@@ -8,7 +8,10 @@
/* eslint-disable no-var */
import {enableSchedulerDebugging} from './SchedulerFeatureFlags';
import {
enableSchedulerDebugging,
enableProfiling,
} from './SchedulerFeatureFlags';
import {
requestHostCallback,
requestHostTimeout,
@@ -21,11 +24,26 @@ import {
import {push, pop, peek} from './SchedulerMinHeap';
// TODO: Use symbols?
var ImmediatePriority = 1;
var UserBlockingPriority = 2;
var NormalPriority = 3;
var LowPriority = 4;
var IdlePriority = 5;
import {
ImmediatePriority,
UserBlockingPriority,
NormalPriority,
LowPriority,
IdlePriority,
} from './SchedulerPriorities';
import {
sharedProfilingBuffer,
markTaskRun,
markTaskYield,
markTaskCompleted,
markTaskCanceled,
markTaskErrored,
markSchedulerSuspended,
markSchedulerUnsuspended,
markTaskStart,
stopLoggingProfilingEvents,
startLoggingProfilingEvents,
} from './SchedulerProfiling';
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
@@ -46,7 +64,7 @@ var taskQueue = [];
var timerQueue = [];
// Incrementing id counter. Used to maintain insertion order.
var taskIdCounter = 0;
var taskIdCounter = 1;
// Pausing the scheduler is useful for debugging.
var isSchedulerPaused = false;
@@ -60,15 +78,6 @@ var isPerformingWork = false;
var isHostCallbackScheduled = false;
var isHostTimeoutScheduled = false;
function flushTask(task, callback, currentTime) {
currentPriorityLevel = task.priorityLevel;
var didUserCallbackTimeout = task.expirationTime <= currentTime;
var continuationCallback = callback(didUserCallbackTimeout);
return typeof continuationCallback === 'function'
? continuationCallback
: null;
}
function advanceTimers(currentTime) {
// Check for tasks that are no longer delayed and add them to the queue.
let timer = peek(timerQueue);
@@ -81,6 +90,10 @@ function advanceTimers(currentTime) {
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer);
timer.isQueued = true;
}
} else {
// Remaining timers are pending.
return;
@@ -107,6 +120,10 @@ function handleTimeout(currentTime) {
}
function flushWork(hasTimeRemaining, initialTime) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
@@ -118,52 +135,82 @@ function flushWork(hasTimeRemaining, initialTime) {
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
const callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
const continuation = flushTask(currentTask, callback, currentTime);
if (continuation !== null) {
currentTask.callback = continuation;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
const currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
currentTime = getCurrentTime();
advanceTimers(currentTime);
} else {
pop(taskQueue);
throw error;
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
let firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
// No catch in prod codepath.
return workLoop(hasTimeRemaining, initialTime);
}
} finally {
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
if (enableProfiling) {
const currentTime = getCurrentTime();
markSchedulerSuspended(currentTime);
}
}
}
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
const callback = currentTask.callback;
if (callback !== null) {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
markTaskRun(currentTask, currentTime);
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
markTaskYield(currentTask, currentTime);
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
let firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
@@ -276,6 +323,9 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {
// This is a delayed task.
@@ -295,6 +345,10 @@ function unstable_scheduleCallback(priorityLevel, callback, options) {
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
@@ -323,9 +377,17 @@ function unstable_getFirstCallbackNode() {
}
function unstable_cancelCallback(task) {
// Null out the callback to indicate the task has been canceled. (Can't remove
// from the queue because you can't remove arbitrary nodes from an array based
// heap, only the first one.)
if (enableProfiling) {
if (task.isQueued) {
const currentTime = getCurrentTime();
markTaskCanceled(task, currentTime);
task.isQueued = false;
}
}
// Null out the callback to indicate the task has been canceled. (Can't
// remove from the queue because you can't remove arbitrary nodes from an
// array based heap, only the first one.)
task.callback = null;
}
@@ -370,3 +432,11 @@ export {
getCurrentTime as unstable_now,
forceFrameRate as unstable_forceFrameRate,
};
export const unstable_Profiling = enableProfiling
? {
startLoggingProfilingEvents,
stopLoggingProfilingEvents,
sharedProfilingBuffer,
}
: null;

View File

@@ -11,3 +11,4 @@ export const enableIsInputPending = false;
export const requestIdleCallbackBeforeFirstFrame = false;
export const requestTimerEventBeforeFirstFrame = false;
export const enableMessageLoopImplementation = false;
export const enableProfiling = __PROFILE__;

View File

@@ -0,0 +1,18 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;

View File

@@ -0,0 +1,203 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
import type {PriorityLevel} from './SchedulerPriorities';
import {enableProfiling} from './SchedulerFeatureFlags';
import {NoPriority} from './SchedulerPriorities';
let runIdCounter: number = 0;
let mainThreadIdCounter: number = 0;
const profilingStateSize = 4;
export const sharedProfilingBuffer = enableProfiling
? // $FlowFixMe Flow doesn't know about SharedArrayBuffer
typeof SharedArrayBuffer === 'function'
? new SharedArrayBuffer(profilingStateSize * Int32Array.BYTES_PER_ELEMENT)
: // $FlowFixMe Flow doesn't know about ArrayBuffer
typeof ArrayBuffer === 'function'
? new ArrayBuffer(profilingStateSize * Int32Array.BYTES_PER_ELEMENT)
: null // Don't crash the init path on IE9
: null;
const profilingState =
enableProfiling && sharedProfilingBuffer !== null
? new Int32Array(sharedProfilingBuffer)
: []; // We can't read this but it helps save bytes for null checks
const PRIORITY = 0;
const CURRENT_TASK_ID = 1;
const CURRENT_RUN_ID = 2;
const QUEUE_SIZE = 3;
if (enableProfiling) {
profilingState[PRIORITY] = NoPriority;
// This is maintained with a counter, because the size of the priority queue
// array might include canceled tasks.
profilingState[QUEUE_SIZE] = 0;
profilingState[CURRENT_TASK_ID] = 0;
}
const INITIAL_EVENT_LOG_SIZE = 1000;
let eventLogSize = 0;
let eventLogBuffer = null;
let eventLog = null;
let eventLogIndex = 0;
const TaskStartEvent = 1;
const TaskCompleteEvent = 2;
const TaskErrorEvent = 3;
const TaskCancelEvent = 4;
const TaskRunEvent = 5;
const TaskYieldEvent = 6;
const SchedulerSuspendEvent = 7;
const SchedulerResumeEvent = 8;
function logEvent(entries) {
if (eventLog !== null) {
const offset = eventLogIndex;
eventLogIndex += entries.length;
if (eventLogIndex + 1 > eventLogSize) {
eventLogSize = eventLogIndex + 1;
const newEventLog = new Int32Array(
eventLogSize * Int32Array.BYTES_PER_ELEMENT,
);
newEventLog.set(eventLog);
eventLogBuffer = newEventLog.buffer;
eventLog = newEventLog;
}
eventLog.set(entries, offset);
}
}
export function startLoggingProfilingEvents(): void {
eventLogSize = INITIAL_EVENT_LOG_SIZE;
eventLogBuffer = new ArrayBuffer(eventLogSize * Int32Array.BYTES_PER_ELEMENT);
eventLog = new Int32Array(eventLogBuffer);
eventLogIndex = 0;
}
export function stopLoggingProfilingEvents(): ArrayBuffer | null {
const buffer = eventLogBuffer;
eventLogBuffer = eventLog = null;
return buffer;
}
export function markTaskStart(
task: {id: number, priorityLevel: PriorityLevel},
time: number,
) {
if (enableProfiling) {
profilingState[QUEUE_SIZE]++;
if (eventLog !== null) {
logEvent([TaskStartEvent, time, task.id, task.priorityLevel]);
}
}
}
export function markTaskCompleted(
task: {
id: number,
priorityLevel: PriorityLevel,
},
time: number,
) {
if (enableProfiling) {
profilingState[PRIORITY] = NoPriority;
profilingState[CURRENT_TASK_ID] = 0;
profilingState[QUEUE_SIZE]--;
if (eventLog !== null) {
logEvent([TaskCompleteEvent, time, task.id]);
}
}
}
export function markTaskCanceled(
task: {
id: number,
priorityLevel: PriorityLevel,
},
time: number,
) {
if (enableProfiling) {
profilingState[QUEUE_SIZE]--;
if (eventLog !== null) {
logEvent([TaskCancelEvent, time, task.id]);
}
}
}
export function markTaskErrored(
task: {
id: number,
priorityLevel: PriorityLevel,
},
time: number,
) {
if (enableProfiling) {
profilingState[PRIORITY] = NoPriority;
profilingState[CURRENT_TASK_ID] = 0;
profilingState[QUEUE_SIZE]--;
if (eventLog !== null) {
logEvent([TaskErrorEvent, time, task.id]);
}
}
}
export function markTaskRun(
task: {id: number, priorityLevel: PriorityLevel},
time: number,
) {
if (enableProfiling) {
runIdCounter++;
profilingState[PRIORITY] = task.priorityLevel;
profilingState[CURRENT_TASK_ID] = task.id;
profilingState[CURRENT_RUN_ID] = runIdCounter;
if (eventLog !== null) {
logEvent([TaskRunEvent, time, task.id, runIdCounter]);
}
}
}
export function markTaskYield(task: {id: number}, time: number) {
if (enableProfiling) {
profilingState[PRIORITY] = NoPriority;
profilingState[CURRENT_TASK_ID] = 0;
profilingState[CURRENT_RUN_ID] = 0;
if (eventLog !== null) {
logEvent([TaskYieldEvent, time, task.id, runIdCounter]);
}
}
}
export function markSchedulerSuspended(time: number) {
if (enableProfiling) {
mainThreadIdCounter++;
if (eventLog !== null) {
logEvent([SchedulerSuspendEvent, time, mainThreadIdCounter]);
}
}
}
export function markSchedulerUnsuspended(time: number) {
if (enableProfiling) {
if (eventLog !== null) {
logEvent([SchedulerResumeEvent, time, mainThreadIdCounter]);
}
}
}

View File

@@ -59,11 +59,15 @@ describe('SchedulerDOM', () => {
runPostMessageCallbacks(config);
}
let frameSize = 33;
let startOfLatestFrame = 0;
let currentTime = 0;
let frameSize;
let startOfLatestFrame;
let currentTime;
beforeEach(() => {
frameSize = 33;
startOfLatestFrame = 0;
currentTime = 0;
delete global.performance;
global.requestAnimationFrame = function(cb) {
return rAFCallbacks.push(() => {

View File

@@ -0,0 +1,508 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @emails react-core
* @jest-environment node
*/
/* eslint-disable no-for-of-loops/no-for-of-loops */
'use strict';
let Scheduler;
let sharedProfilingArray;
// let runWithPriority;
let ImmediatePriority;
let UserBlockingPriority;
let NormalPriority;
let LowPriority;
let IdlePriority;
let scheduleCallback;
let cancelCallback;
// let wrapCallback;
// let getCurrentPriorityLevel;
// let shouldYield;
function priorityLevelToString(priorityLevel) {
switch (priorityLevel) {
case ImmediatePriority:
return 'Immediate';
case UserBlockingPriority:
return 'User-blocking';
case NormalPriority:
return 'Normal';
case LowPriority:
return 'Low';
case IdlePriority:
return 'Idle';
default:
return null;
}
}
describe('Scheduler', () => {
if (!__PROFILE__) {
// The tests in this suite only apply when profiling is on
it('profiling APIs are not available', () => {
Scheduler = require('scheduler');
expect(Scheduler.unstable_Profiling).toBe(null);
});
return;
}
beforeEach(() => {
jest.resetModules();
jest.mock('scheduler', () => require('scheduler/unstable_mock'));
Scheduler = require('scheduler');
sharedProfilingArray = new Int32Array(
Scheduler.unstable_Profiling.sharedProfilingBuffer,
);
// runWithPriority = Scheduler.unstable_runWithPriority;
ImmediatePriority = Scheduler.unstable_ImmediatePriority;
UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
NormalPriority = Scheduler.unstable_NormalPriority;
LowPriority = Scheduler.unstable_LowPriority;
IdlePriority = Scheduler.unstable_IdlePriority;
scheduleCallback = Scheduler.unstable_scheduleCallback;
cancelCallback = Scheduler.unstable_cancelCallback;
// wrapCallback = Scheduler.unstable_wrapCallback;
// getCurrentPriorityLevel = Scheduler.unstable_getCurrentPriorityLevel;
// shouldYield = Scheduler.unstable_shouldYield;
});
const PRIORITY = 0;
const CURRENT_TASK_ID = 1;
const CURRENT_RUN_ID = 2;
const QUEUE_SIZE = 3;
afterEach(() => {
if (sharedProfilingArray[QUEUE_SIZE] !== 0) {
throw Error(
'Test exited, but the shared profiling buffer indicates that a task ' +
'is still running',
);
}
});
const TaskStartEvent = 1;
const TaskCompleteEvent = 2;
const TaskErrorEvent = 3;
const TaskCancelEvent = 4;
const TaskRunEvent = 5;
const TaskYieldEvent = 6;
const SchedulerSuspendEvent = 7;
const SchedulerResumeEvent = 8;
function stopProfilingAndPrintFlamegraph() {
const eventLog = new Int32Array(
Scheduler.unstable_Profiling.stopLoggingProfilingEvents(),
);
const tasks = new Map();
const mainThreadRuns = [];
let isSuspended = true;
let i = 0;
processLog: while (i < eventLog.length) {
const instruction = eventLog[i];
const time = eventLog[i + 1];
switch (instruction) {
case 0: {
break processLog;
}
case TaskStartEvent: {
const taskId = eventLog[i + 2];
const priorityLevel = eventLog[i + 3];
const task = {
id: taskId,
priorityLevel,
label: null,
start: time,
end: -1,
exitStatus: null,
runs: [],
};
tasks.set(taskId, task);
i += 4;
break;
}
case TaskCompleteEvent: {
if (isSuspended) {
throw Error('Task cannot Complete outside the work loop.');
}
const taskId = eventLog[i + 2];
const task = tasks.get(taskId);
if (task === undefined) {
throw Error('Task does not exist.');
}
task.end = time;
task.exitStatus = 'completed';
i += 3;
break;
}
case TaskErrorEvent: {
if (isSuspended) {
throw Error('Task cannot Error outside the work loop.');
}
const taskId = eventLog[i + 2];
const task = tasks.get(taskId);
if (task === undefined) {
throw Error('Task does not exist.');
}
task.end = time;
task.exitStatus = 'errored';
i += 3;
break;
}
case TaskCancelEvent: {
const taskId = eventLog[i + 2];
const task = tasks.get(taskId);
if (task === undefined) {
throw Error('Task does not exist.');
}
task.end = time;
task.exitStatus = 'canceled';
i += 3;
break;
}
case TaskRunEvent:
case TaskYieldEvent: {
if (isSuspended) {
throw Error('Task cannot Run or Yield outside the work loop.');
}
const taskId = eventLog[i + 2];
const task = tasks.get(taskId);
if (task === undefined) {
throw Error('Task does not exist.');
}
task.runs.push(time);
i += 4;
break;
}
case SchedulerSuspendEvent: {
if (isSuspended) {
throw Error('Scheduler cannot Suspend outside the work loop.');
}
isSuspended = true;
mainThreadRuns.push(time);
i += 3;
break;
}
case SchedulerResumeEvent: {
if (!isSuspended) {
throw Error('Scheduler cannot Resume inside the work loop.');
}
isSuspended = false;
mainThreadRuns.push(time);
i += 3;
break;
}
default: {
throw Error('Unknown instruction type: ' + instruction);
}
}
}
// Now we can render the tasks as a flamegraph.
const labelColumnWidth = 30;
const msPerChar = 50;
let result = '';
const mainThreadLabelColumn = '!!! Main thread ';
let mainThreadTimelineColumn = '';
let isMainThreadBusy = true;
for (const time of mainThreadRuns) {
const index = time / msPerChar;
mainThreadTimelineColumn += (isMainThreadBusy ? '█' : '░').repeat(
index - mainThreadTimelineColumn.length,
);
isMainThreadBusy = !isMainThreadBusy;
}
result += `${mainThreadLabelColumn}${mainThreadTimelineColumn}\n`;
const tasksByPriority = Array.from(tasks.values()).sort(
(t1, t2) => t1.priorityLevel - t2.priorityLevel,
);
for (const task of tasksByPriority) {
let label = task.label;
if (label === undefined) {
label = 'Task';
}
let labelColumn = `Task ${task.id} [${priorityLevelToString(
task.priorityLevel,
)}]`;
labelColumn += ' '.repeat(labelColumnWidth - labelColumn.length - 1);
// Add empty space up until the start mark
let timelineColumn = ' '.repeat(task.start / msPerChar);
let isRunning = false;
for (const time of task.runs) {
const index = time / msPerChar;
timelineColumn += (isRunning ? '█' : '░').repeat(
index - timelineColumn.length,
);
isRunning = !isRunning;
}
const endIndex = task.end / msPerChar;
timelineColumn += (isRunning ? '█' : '░').repeat(
endIndex - timelineColumn.length,
);
if (task.exitStatus !== 'completed') {
timelineColumn += `🡐 ${task.exitStatus}`;
}
result += `${labelColumn}${timelineColumn}\n`;
}
return '\n' + result;
}
function getProfilingInfo() {
const queueSize = sharedProfilingArray[QUEUE_SIZE];
if (queueSize === 0) {
return 'Empty Queue';
}
const priorityLevel = sharedProfilingArray[PRIORITY];
if (priorityLevel === 0) {
return 'Suspended, Queue Size: ' + queueSize;
}
return (
`Task: ${sharedProfilingArray[CURRENT_TASK_ID]}, ` +
`Run: ${sharedProfilingArray[CURRENT_RUN_ID]}, ` +
`Priority: ${priorityLevelToString(priorityLevel)}, ` +
`Queue Size: ${sharedProfilingArray[QUEUE_SIZE]}`
);
}
it('creates a basic flamegraph', () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
Scheduler.unstable_advanceTime(100);
scheduleCallback(
NormalPriority,
() => {
Scheduler.unstable_advanceTime(300);
Scheduler.unstable_yieldValue(getProfilingInfo());
scheduleCallback(
UserBlockingPriority,
() => {
Scheduler.unstable_yieldValue(getProfilingInfo());
Scheduler.unstable_advanceTime(300);
},
{label: 'Bar'},
);
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_yieldValue('Yield');
return () => {
Scheduler.unstable_yieldValue(getProfilingInfo());
Scheduler.unstable_advanceTime(300);
};
},
{label: 'Foo'},
);
expect(Scheduler).toFlushAndYieldThrough([
'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
'Yield',
]);
Scheduler.unstable_advanceTime(100);
expect(Scheduler).toFlushAndYield([
'Task: 2, Run: 2, Priority: User-blocking, Queue Size: 2',
'Task: 1, Run: 3, Priority: Normal, Queue Size: 1',
]);
expect(getProfilingInfo()).toEqual('Empty Queue');
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │██░░░░░░░░██░░░░░░░░░░░░
Task 2 [User-blocking] │ ░░░░██████
Task 1 [Normal] │ ████████░░░░░░░░██████
`,
);
});
it('marks when a task is canceled', () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue(getProfilingInfo());
Scheduler.unstable_advanceTime(300);
Scheduler.unstable_yieldValue('Yield');
return () => {
Scheduler.unstable_yieldValue('Continuation');
Scheduler.unstable_advanceTime(200);
};
});
expect(Scheduler).toFlushAndYieldThrough([
'Task: 1, Run: 1, Priority: Normal, Queue Size: 1',
'Yield',
]);
Scheduler.unstable_advanceTime(100);
cancelCallback(task);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushWithoutYielding();
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │░░░░░░██████████████████████
Task 1 [Normal] │██████░░🡐 canceled
`,
);
});
it('marks when a task errors', () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
scheduleCallback(NormalPriority, () => {
Scheduler.unstable_advanceTime(300);
throw Error('Oops');
});
expect(Scheduler).toFlushAndThrow('Oops');
Scheduler.unstable_advanceTime(100);
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushWithoutYielding();
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │░░░░░░██████████████████████
Task 1 [Normal] │██████🡐 errored
`,
);
});
it('marks when multiple tasks are canceled', () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task1 = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue(getProfilingInfo());
Scheduler.unstable_advanceTime(300);
Scheduler.unstable_yieldValue('Yield');
return () => {
Scheduler.unstable_yieldValue('Continuation');
Scheduler.unstable_advanceTime(200);
};
});
const task2 = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue(getProfilingInfo());
Scheduler.unstable_advanceTime(300);
Scheduler.unstable_yieldValue('Yield');
return () => {
Scheduler.unstable_yieldValue('Continuation');
Scheduler.unstable_advanceTime(200);
};
});
expect(Scheduler).toFlushAndYieldThrough([
'Task: 1, Run: 1, Priority: Normal, Queue Size: 2',
'Yield',
]);
Scheduler.unstable_advanceTime(100);
cancelCallback(task1);
cancelCallback(task2);
// Advance more time. This should not affect the size of the main
// thread row, since the Scheduler queue is empty.
Scheduler.unstable_advanceTime(1000);
expect(Scheduler).toFlushWithoutYielding();
// The main thread row should end when the callback is cancelled.
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │░░░░░░██████████████████████
Task 1 [Normal] │██████░░🡐 canceled
Task 2 [Normal] │░░░░░░░░🡐 canceled
`,
);
});
it('handles cancelling a task that already finished', () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task = scheduleCallback(NormalPriority, () => {
Scheduler.unstable_yieldValue('A');
Scheduler.unstable_advanceTime(1000);
});
expect(Scheduler).toFlushAndYield(['A']);
cancelCallback(task);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │░░░░░░░░░░░░░░░░░░░░
Task 1 [Normal] │████████████████████
`,
);
});
it('handles cancelling a task multiple times', () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
scheduleCallback(
NormalPriority,
() => {
Scheduler.unstable_yieldValue('A');
Scheduler.unstable_advanceTime(1000);
},
{label: 'A'},
);
Scheduler.unstable_advanceTime(200);
const task = scheduleCallback(
NormalPriority,
() => {
Scheduler.unstable_yieldValue('B');
Scheduler.unstable_advanceTime(1000);
},
{label: 'B'},
);
Scheduler.unstable_advanceTime(400);
cancelCallback(task);
cancelCallback(task);
cancelCallback(task);
expect(Scheduler).toFlushAndYield(['A']);
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │████████████░░░░░░░░░░░░░░░░░░░░
Task 1 [Normal] │░░░░░░░░░░░░████████████████████
Task 2 [Normal] │ ░░░░░░░░🡐 canceled
`,
);
});
it('handles cancelling a delayed task', () => {
Scheduler.unstable_Profiling.startLoggingProfilingEvents();
const task = scheduleCallback(
NormalPriority,
() => Scheduler.unstable_yieldValue('A'),
{delay: 1000},
);
cancelCallback(task);
expect(Scheduler).toFlushWithoutYielding();
expect(stopProfilingAndPrintFlamegraph()).toEqual(
`
!!! Main thread │
`,
);
});
it('resizes event log buffer if there are many events', () => {
const tasks = [];
for (let i = 0; i < 5000; i++) {
tasks.push(scheduleCallback(NormalPriority, () => {}));
}
expect(getProfilingInfo()).toEqual('Suspended, Queue Size: 5000');
tasks.forEach(task => cancelCallback(task));
expect(getProfilingInfo()).toEqual('Empty Queue');
});
});

View File

@@ -13,3 +13,5 @@ export const {
requestTimerEventBeforeFirstFrame,
enableMessageLoopImplementation,
} = require('SchedulerFeatureFlags');
export const enableProfiling = __PROFILE__;

View File

@@ -53,8 +53,9 @@ if (
}
}
};
const initialTime = Date.now();
getCurrentTime = function() {
return Date.now();
return Date.now() - initialTime;
};
requestHostCallback = function(cb) {
if (_callback !== null) {
@@ -111,10 +112,15 @@ if (
typeof requestIdleCallback === 'function' &&
typeof cancelIdleCallback === 'function';
getCurrentTime =
typeof performance === 'object' && typeof performance.now === 'function'
? () => performance.now()
: () => Date.now();
if (
typeof performance === 'object' &&
typeof performance.now === 'function'
) {
getCurrentTime = () => performance.now();
} else {
const initialTime = Date.now();
getCurrentTime = () => Date.now() - initialTime;
}
let isRAFLoopRunning = false;
let isMessageLoopRunning = false;

View File

@@ -10,26 +10,28 @@
export type SideEffectTag = number;
// Don't change these two values. They're used by React Dev Tools.
export const NoEffect = /* */ 0b000000000000;
export const PerformedWork = /* */ 0b000000000001;
export const NoEffect = /* */ 0b0000000000000;
export const PerformedWork = /* */ 0b0000000000001;
// You can change the rest (and add more).
export const Placement = /* */ 0b000000000010;
export const Update = /* */ 0b000000000100;
export const PlacementAndUpdate = /* */ 0b000000000110;
export const Deletion = /* */ 0b000000001000;
export const ContentReset = /* */ 0b000000010000;
export const Callback = /* */ 0b000000100000;
export const DidCapture = /* */ 0b000001000000;
export const Ref = /* */ 0b000010000000;
export const Snapshot = /* */ 0b000100000000;
export const Passive = /* */ 0b001000000000;
export const Placement = /* */ 0b0000000000010;
export const Update = /* */ 0b0000000000100;
export const PlacementAndUpdate = /* */ 0b0000000000110;
export const Deletion = /* */ 0b0000000001000;
export const ContentReset = /* */ 0b0000000010000;
export const Callback = /* */ 0b0000000100000;
export const DidCapture = /* */ 0b0000001000000;
export const Ref = /* */ 0b0000010000000;
export const Snapshot = /* */ 0b0000100000000;
export const Passive = /* */ 0b0001000000000;
export const Hydrating = /* */ 0b0010000000000;
export const HydratingAndUpdate = /* */ 0b0010000000100;
// Passive & Update & Callback & Ref & Snapshot
export const LifecycleEffectMask = /* */ 0b001110100100;
export const LifecycleEffectMask = /* */ 0b0001110100100;
// Union of all host effects
export const HostEffectMask = /* */ 0b001111111111;
export const HostEffectMask = /* */ 0b0011111111111;
export const Incomplete = /* */ 0b010000000000;
export const ShouldCapture = /* */ 0b100000000000;
export const Incomplete = /* */ 0b0100000000000;
export const ShouldCapture = /* */ 0b1000000000000;

View File

@@ -411,7 +411,13 @@ const bundles = [
/******* React Scheduler (experimental) *******/
{
bundleTypes: [NODE_DEV, NODE_PROD, FB_WWW_DEV, FB_WWW_PROD],
bundleTypes: [
NODE_DEV,
NODE_PROD,
FB_WWW_DEV,
FB_WWW_PROD,
FB_WWW_PROFILING,
],
moduleType: ISOMORPHIC,
entry: 'scheduler',
global: 'Scheduler',

View File

@@ -21,6 +21,11 @@ module.exports = {
process: true,
setImmediate: true,
Buffer: true,
// Scheduler profiling
SharedArrayBuffer: true,
Int32Array: true,
ArrayBuffer: true,
},
parserOptions: {
ecmaVersion: 5,

View File

@@ -22,6 +22,11 @@ module.exports = {
// Node.js Server Rendering
setImmediate: true,
Buffer: true,
// Scheduler profiling
SharedArrayBuffer: true,
Int32Array: true,
ArrayBuffer: true,
},
parserOptions: {
ecmaVersion: 5,

View File

@@ -21,6 +21,11 @@ module.exports = {
// Fabric. See https://github.com/facebook/react/pull/15490
// for more information
nativeFabricUIManager: true,
// Scheduler profiling
SharedArrayBuffer: true,
Int32Array: true,
ArrayBuffer: true,
},
parserOptions: {
ecmaVersion: 5,

View File

@@ -24,6 +24,11 @@ module.exports = {
define: true,
require: true,
global: true,
// Scheduler profiling
SharedArrayBuffer: true,
Int32Array: true,
ArrayBuffer: true,
},
parserOptions: {
ecmaVersion: 5,