From 7ccaa6bb214fe254aa4c47ea23275501617d71ca Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 6 Jan 2021 11:46:24 +0100 Subject: [PATCH 1/3] fix: update implementaion of getReactFiberFromNode to be compatible with React 17 --- .../src/components/Debug/FiberNavigator.ts | 256 +---------------- .../src/utils/getReactFiberFromNode.ts | 259 ++++++++++++++++++ .../src/utils/positioner/Popper.tsx | 34 +-- .../utils/getReactFiberFromNode-test.tsx | 29 ++ 4 files changed, 302 insertions(+), 276 deletions(-) create mode 100644 packages/fluentui/react-northstar/src/utils/getReactFiberFromNode.ts create mode 100644 packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx diff --git a/packages/fluentui/react-northstar/src/components/Debug/FiberNavigator.ts b/packages/fluentui/react-northstar/src/components/Debug/FiberNavigator.ts index 8c14e872fb3ce..264aa73bfdd2e 100644 --- a/packages/fluentui/react-northstar/src/components/Debug/FiberNavigator.ts +++ b/packages/fluentui/react-northstar/src/components/Debug/FiberNavigator.ts @@ -1,263 +1,11 @@ -import * as React from 'react'; import { ForwardRef } from 'react-is'; - -// ======================================================== -// react/packages/shared/ReactTypes.js -// ======================================================== - -type ReactEventResponder = { - $$typeof: Symbol | number; - displayName: string; - targetEventTypes: null | string[]; - rootEventTypes: null | string[]; - getInitialState: null | ((props: Object) => Object); - onEvent: null | ((event: E, context: C, props: Object, state: Object) => void); - onRootEvent: null | ((event: E, context: C, props: Object, state: Object) => void); - onMount: null | ((context: C, props: Object, state: Object) => void); - onUnmount: null | ((context: C, props: Object, state: Object) => void); -}; - -type ReactEventResponderInstance = { - fiber: Object; - props: Object; - responder: ReactEventResponder; - rootEventTypes: null | Set; - state: Object; -}; - -// ======================================================== -// react/packages/react-reconciler/src/ReactFiberHooks.js -// ======================================================== - -export type HookType = - | 'useState' - | 'useReducer' - | 'useContext' - | 'useRef' - | 'useEffect' - | 'useLayoutEffect' - | 'useCallback' - | 'useMemo' - | 'useImperativeHandle' - | 'useDebugValue' - | 'useResponder'; - -type ReactProviderType = { - $$typeof: Symbol | number; - _context: ReactContext; -}; - -type ReactContext = { - $$typeof: Symbol | number; - Consumer: ReactContext; - Provider: ReactProviderType; - - _calculateChangedBits: ((a: T, b: T) => number) | null; - - _currentValue: T; - _currentValue2: T; - _threadCount: number; - - // DEV only - _currentRenderer?: Object | null; - _currentRenderer2?: Object | null; -}; - -type ContextDependency = { - context: ReactContext; - observedBits: number; - next: ContextDependency | null; -}; - -enum WorkTag { - FunctionComponent = 0, - ClassComponent = 1, - IndeterminateComponent = 2, // Before we know whether it is function or class - HostRoot = 3, // Root of a host tree. Could be nested inside another node. - HostPortal = 4, // A subtree. Could be an entry point to a different renderer. - HostComponent = 5, - HostText = 6, - Fragment = 7, - Mode = 8, - ContextConsumer = 9, - ContextProvider = 10, - ForwardRef = 11, - Profiler = 12, - SuspenseComponent = 13, - MemoComponent = 14, - SimpleMemoComponent = 15, - LazyComponent = 16, - IncompleteClassComponent = 17, - DehydratedFragment = 18, - SuspenseListComponent = 19, - FundamentalComponent = 20, - ScopeComponent = 21, -} - -type Source = { - fileName: string; - lineNumber: number; -}; - -type ExpirationTime = number; - -type Dependencies = { - expirationTime: ExpirationTime; - firstContext: ContextDependency | null; - responders: Map, ReactEventResponderInstance> | null; -}; - -// ======================================================== -// react/packages/react-reconciler/src/ReactFiber.js -// ======================================================== - -// A Fiber is work on a Component that needs to be done or was done. There can -// be more than one per component. -type Fiber = { - // These first fields are conceptually members of an Instance. This used to - // be split into a separate type and intersected with the other Fiber fields, - // but until Flow fixes its intersection bugs, we've merged them into a - // single type. - - // An Instance is shared between all versions of a component. We can easily - // break this out into a separate object to avoid copying so much to the - // alternate versions of the tree. We put this on a single object for now to - // minimize the number of objects created during the initial render. - - // Tag identifying the type of fiber. - tag: WorkTag; - - // Unique identifier of this child. - key: null | string; - - // The value of element.type which is used to preserve the identity during - // reconciliation of this child. - elementType: any; - - // The resolved function/class/ associated with this fiber. - type: any; - - // The local state associated with this fiber. - stateNode: any; - - // Conceptual aliases - // parent : Instance -> return The parent happens to be the same as the - // return fiber since we've merged the fiber and instance. - - // Remaining fields belong to Fiber - - // The Fiber to return to after finishing processing this one. - // This is effectively the parent, but there can be multiple parents (two) - // so this is only the parent of the thing we're currently processing. - // It is conceptually the same as the return address of a stack frame. - return: Fiber | null; - - // Singly Linked List Tree Structure. - child: Fiber | null; - sibling: Fiber | null; - index: number; - - // The ref last used to attach this node. - // I'll avoid adding an owner field for prod and model that as functions. - ref: React.Ref; - - // Input is the data coming into process this fiber. Arguments. Props. - pendingProps: any; // This type will be more specific once we overload the tag. - memoizedProps: any; // The props used to create the output. - - // A queue of state updates and callbacks. - // updateQueue: UpdateQueue | null, - - // The state used to create the output - memoizedState: any; - - // Dependencies (contexts, events) for this fiber, if it has any - dependencies: Dependencies | null; - - // // Bitfield that describes properties about the fiber and its subtree. E.g. - // // the ConcurrentMode flag indicates whether the subtree should be async-by- - // // default. When a fiber is created, it inherits the mode of its - // // parent. Additional flags can be set at creation time, but after that the - // // value should remain unchanged throughout the fiber's lifetime, particularly - // // before its child fibers are created. - // mode: TypeOfMode - // - // // Effect - // effectTag: SideEffectTag - - // Singly linked list fast path to the next fiber with side-effects. - nextEffect: Fiber | null; - - // The first and last fiber with side-effect within this subtree. This allows - // us to reuse a slice of the linked list when we reuse the work done within - // this fiber. - firstEffect: Fiber | null; - lastEffect: Fiber | null; - - // Represents a time in the future by which this work should be completed. - // Does not include work found in its subtree. - expirationTime: ExpirationTime; - - // This is used to quickly determine if a subtree has no pending changes. - childExpirationTime: ExpirationTime; - - // This is a pooled version of a Fiber. Every fiber that gets updated will - // eventually have a pair. There are cases when we can clean up pairs to save - // memory if we need to. - alternate: Fiber | null; - - // Time spent rendering this Fiber and its descendants for the current update. - // This tells us how well the tree makes use of sCU for memoization. - // It is reset to 0 each time we render and only updated when we don't bailout. - // This field is only set when the enableProfilerTimer flag is enabled. - actualDuration?: number; - - // If the Fiber is currently active in the "render" phase, - // This marks the time at which the work began. - // This field is only set when the enableProfilerTimer flag is enabled. - actualStartTime?: number; - - // Duration of the most recent render time for this Fiber. - // This value is not updated when we bailout for memoization purposes. - // This field is only set when the enableProfilerTimer flag is enabled. - selfBaseDuration?: number; - - // Sum of base times for all descendants of this Fiber. - // This value bubbles up during the "complete" phase. - // This field is only set when the enableProfilerTimer flag is enabled. - treeBaseDuration?: number; - - // Conceptual aliases - // workInProgress : Fiber -> alternate The alternate used for reuse happens - // to be the same as work in progress. - // __DEV__ only - _debugID?: number; - _debugSource?: Source | null; - _debugOwner?: Fiber | null; - _debugIsCurrentlyTiming?: boolean; - _debugNeedsRemount?: boolean; - - // Used to verify that the order of hooks does not change between renders. - _debugHookTypes?: HookType[] | null; -}; +import { getReactFiberFromNode, Fiber } from '../../utils/getReactFiberFromNode'; const isDOMNode = e => e && typeof e.tagName === 'string' && e.nodeType === Node.ELEMENT_NODE; export class FiberNavigator { __fiber: Fiber; - static domNodeToReactFiber = (elm: HTMLElement): Fiber => { - if (!elm) return null; - - for (const k in elm) { - if (k.startsWith('__reactInternalInstance$')) { - return elm[k]; - } - } - - return null; - }; - // TODO: Fibers can become stale. // The only current fiber is the one found on the DOM node. // There is no way to start at a React Component fiber, go the DOM node, @@ -279,7 +27,7 @@ export class FiberNavigator { }; static fromDOMNode = domNode => { - const fiber = FiberNavigator.domNodeToReactFiber(domNode); + const fiber = getReactFiberFromNode(domNode); if (!fiber) return null; diff --git a/packages/fluentui/react-northstar/src/utils/getReactFiberFromNode.ts b/packages/fluentui/react-northstar/src/utils/getReactFiberFromNode.ts new file mode 100644 index 0000000000000..9fc51de3c9725 --- /dev/null +++ b/packages/fluentui/react-northstar/src/utils/getReactFiberFromNode.ts @@ -0,0 +1,259 @@ +import * as React from 'react'; + +// ======================================================== +// react/packages/shared/ReactTypes.js +// ======================================================== + +type ReactEventResponder = { + $$typeof: Symbol | number; + displayName: string; + targetEventTypes: null | string[]; + rootEventTypes: null | string[]; + getInitialState: null | ((props: Object) => Object); + onEvent: null | ((event: E, context: C, props: Object, state: Object) => void); + onRootEvent: null | ((event: E, context: C, props: Object, state: Object) => void); + onMount: null | ((context: C, props: Object, state: Object) => void); + onUnmount: null | ((context: C, props: Object, state: Object) => void); +}; + +type ReactEventResponderInstance = { + fiber: Object; + props: Object; + responder: ReactEventResponder; + rootEventTypes: null | Set; + state: Object; +}; + +// ======================================================== +// react/packages/react-reconciler/src/ReactFiberHooks.js +// ======================================================== + +export type HookType = + | 'useState' + | 'useReducer' + | 'useContext' + | 'useRef' + | 'useEffect' + | 'useLayoutEffect' + | 'useCallback' + | 'useMemo' + | 'useImperativeHandle' + | 'useDebugValue' + | 'useResponder'; + +type ReactProviderType = { + $$typeof: Symbol | number; + _context: ReactContext; +}; + +type ReactContext = { + $$typeof: Symbol | number; + Consumer: ReactContext; + Provider: ReactProviderType; + + _calculateChangedBits: ((a: T, b: T) => number) | null; + + _currentValue: T; + _currentValue2: T; + _threadCount: number; + + // DEV only + _currentRenderer?: Object | null; + _currentRenderer2?: Object | null; +}; + +type ContextDependency = { + context: ReactContext; + observedBits: number; + next: ContextDependency | null; +}; + +enum WorkTag { + FunctionComponent = 0, + ClassComponent = 1, + IndeterminateComponent = 2, // Before we know whether it is function or class + HostRoot = 3, // Root of a host tree. Could be nested inside another node. + HostPortal = 4, // A subtree. Could be an entry point to a different renderer. + HostComponent = 5, + HostText = 6, + Fragment = 7, + Mode = 8, + ContextConsumer = 9, + ContextProvider = 10, + ForwardRef = 11, + Profiler = 12, + SuspenseComponent = 13, + MemoComponent = 14, + SimpleMemoComponent = 15, + LazyComponent = 16, + IncompleteClassComponent = 17, + DehydratedFragment = 18, + SuspenseListComponent = 19, + FundamentalComponent = 20, + ScopeComponent = 21, +} + +type Source = { + fileName: string; + lineNumber: number; +}; + +type ExpirationTime = number; + +type Dependencies = { + expirationTime: ExpirationTime; + firstContext: ContextDependency | null; + responders: Map, ReactEventResponderInstance> | null; +}; + +// ======================================================== +// react/packages/react-reconciler/src/ReactFiber.js +// ======================================================== + +// A Fiber is work on a Component that needs to be done or was done. There can +// be more than one per component. + +export type Fiber = { + // These first fields are conceptually members of an Instance. This used to + // be split into a separate type and intersected with the other Fiber fields, + // but until Flow fixes its intersection bugs, we've merged them into a + // single type. + + // An Instance is shared between all versions of a component. We can easily + // break this out into a separate object to avoid copying so much to the + // alternate versions of the tree. We put this on a single object for now to + // minimize the number of objects created during the initial render. + + // Tag identifying the type of fiber. + tag: WorkTag; + + // Unique identifier of this child. + key: null | string; + + // The value of element.type which is used to preserve the identity during + // reconciliation of this child. + elementType: any; + + // The resolved function/class/ associated with this fiber. + type: any; + + // The local state associated with this fiber. + stateNode: any; + + // Conceptual aliases + // parent : Instance -> return The parent happens to be the same as the + // return fiber since we've merged the fiber and instance. + + // Remaining fields belong to Fiber + + // The Fiber to return to after finishing processing this one. + // This is effectively the parent, but there can be multiple parents (two) + // so this is only the parent of the thing we're currently processing. + // It is conceptually the same as the return address of a stack frame. + return: Fiber | null; + + // Singly Linked List Tree Structure. + child: Fiber | null; + sibling: Fiber | null; + index: number; + + // The ref last used to attach this node. + // I'll avoid adding an owner field for prod and model that as functions. + ref: React.Ref; + + // Input is the data coming into process this fiber. Arguments. Props. + pendingProps: any; // This type will be more specific once we overload the tag. + memoizedProps: any; // The props used to create the output. + + // A queue of state updates and callbacks. + // updateQueue: UpdateQueue | null, + + // The state used to create the output + memoizedState: any; + + // Dependencies (contexts, events) for this fiber, if it has any + dependencies: Dependencies | null; + + // // Bitfield that describes properties about the fiber and its subtree. E.g. + // // the ConcurrentMode flag indicates whether the subtree should be async-by- + // // default. When a fiber is created, it inherits the mode of its + // // parent. Additional flags can be set at creation time, but after that the + // // value should remain unchanged throughout the fiber's lifetime, particularly + // // before its child fibers are created. + // mode: TypeOfMode + // + // // Effect + // effectTag: SideEffectTag + + // Singly linked list fast path to the next fiber with side-effects. + nextEffect: Fiber | null; + + // The first and last fiber with side-effect within this subtree. This allows + // us to reuse a slice of the linked list when we reuse the work done within + // this fiber. + firstEffect: Fiber | null; + lastEffect: Fiber | null; + + // Represents a time in the future by which this work should be completed. + // Does not include work found in its subtree. + expirationTime: ExpirationTime; + + // This is used to quickly determine if a subtree has no pending changes. + childExpirationTime: ExpirationTime; + + // This is a pooled version of a Fiber. Every fiber that gets updated will + // eventually have a pair. There are cases when we can clean up pairs to save + // memory if we need to. + alternate: Fiber | null; + + // Time spent rendering this Fiber and its descendants for the current update. + // This tells us how well the tree makes use of sCU for memoization. + // It is reset to 0 each time we render and only updated when we don't bailout. + // This field is only set when the enableProfilerTimer flag is enabled. + actualDuration?: number; + + // If the Fiber is currently active in the "render" phase, + // This marks the time at which the work began. + // This field is only set when the enableProfilerTimer flag is enabled. + actualStartTime?: number; + + // Duration of the most recent render time for this Fiber. + // This value is not updated when we bailout for memoization purposes. + // This field is only set when the enableProfilerTimer flag is enabled. + selfBaseDuration?: number; + + // Sum of base times for all descendants of this Fiber. + // This value bubbles up during the "complete" phase. + // This field is only set when the enableProfilerTimer flag is enabled. + treeBaseDuration?: number; + + // Conceptual aliases + // workInProgress : Fiber -> alternate The alternate used for reuse happens + // to be the same as work in progress. + // __DEV__ only + _debugID?: number; + _debugSource?: Source | null; + _debugOwner?: Fiber | null; + _debugIsCurrentlyTiming?: boolean; + _debugNeedsRemount?: boolean; + + // Used to verify that the order of hooks does not change between renders. + _debugHookTypes?: HookType[] | null; +}; + +export function getReactFiberFromNode(elm: Node | undefined): Fiber | null { + if (!elm) { + return null; + } + + for (const k in elm) { + // React 16 uses "__reactInternalInstance$" prefix + // React 17 uses "__reactFiber$" prefix + // https://github.com/facebook/react/pull/18377 + if (k.indexOf('__reactInternalInstance$') === 0 || k.indexOf('__reactFiber$') === 0) { + return elm[k]; + } + } + + throw new Error('getReactFiber(): Failed to find a React Fiber on a node'); +} diff --git a/packages/fluentui/react-northstar/src/utils/positioner/Popper.tsx b/packages/fluentui/react-northstar/src/utils/positioner/Popper.tsx index 29a84ee50c4d0..549088383c2c5 100644 --- a/packages/fluentui/react-northstar/src/utils/positioner/Popper.tsx +++ b/packages/fluentui/react-northstar/src/utils/positioner/Popper.tsx @@ -4,36 +4,26 @@ import * as PopperJs from '@popperjs/core'; import * as _ from 'lodash'; import * as React from 'react'; +import { getReactFiberFromNode } from '../getReactFiberFromNode'; import { isBrowser } from '../isBrowser'; import { getBoundary } from './getBoundary'; import { getScrollParent } from './getScrollParent'; import { getPlacement, applyRtlToOffset } from './positioningHelper'; import { PopperModifiers, PopperProps, PopperPositionFix, PopperJsInstance } from './types'; -let reactInstanceKey: string; - -const getReactInstanceKey = (elm: Node): string => { - if (!reactInstanceKey) { - for (const k in elm) { - if (k.indexOf('__reactInternalInstance$') === 0) { - reactInstanceKey = k; - break; - } - } - } - - return reactInstanceKey; -}; - const hasAutofocusProp = (node: Node): boolean | undefined => { // https://github.com/facebook/react/blob/848bb2426e44606e0a55dfe44c7b3ece33772485/packages/react-dom/src/client/ReactDOMHostConfig.js#L157-L166 - return ( - (node.nodeName === 'BUTTON' || - node.nodeName === 'INPUT' || - node.nodeName === 'SELECT' || - node.nodeName === 'TEXTAREA') && - node[getReactInstanceKey(node)].pendingProps.autoFocus - ); + const isAutoFocusableElement = + node.nodeName === 'BUTTON' || + node.nodeName === 'INPUT' || + node.nodeName === 'SELECT' || + node.nodeName === 'TEXTAREA'; + + if (isAutoFocusableElement) { + return getReactFiberFromNode(node).pendingProps.autoFocus; + } + + return false; }; function hasAutofocusFilter(node: Node) { diff --git a/packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx b/packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx new file mode 100644 index 0000000000000..b881b9c8a6864 --- /dev/null +++ b/packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import * as ReactDOM from 'react-dom'; + +import { getReactFiberFromNode } from 'src/utils/getReactFiberFromNode'; + +describe('getReactFiberFromNode', () => { + it('returns a Fiber for DOM node', () => { + const rootEl = document.createElement('div'); + document.body.appendChild(rootEl); + + ReactDOM.render(
, rootEl); + + const renderedEl = document.querySelector('#foo'); + const fiber = getReactFiberFromNode(renderedEl); + + expect(renderedEl).toBeInTheDocument(); + + expect(fiber).toHaveProperty('tag', 5); + expect(fiber).toHaveProperty('type', 'div'); + expect(fiber).toHaveProperty('pendingProps', expect.objectContaining({ id: 'foo' })); + + ReactDOM.unmountComponentAtNode(rootEl); + document.body.removeChild(rootEl); + }); + + it('returns null if nothing was passed', () => { + expect(getReactFiberFromNode(undefined)).toBe(null); + }); +}); From c2a39b1ff0410d3e20f6893b5a36c31f25a875c7 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 6 Jan 2021 12:12:06 +0100 Subject: [PATCH 2/3] add changelog entry --- packages/fluentui/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/fluentui/CHANGELOG.md b/packages/fluentui/CHANGELOG.md index 9bd4ee00957b4..3ea0ebd963687 100644 --- a/packages/fluentui/CHANGELOG.md +++ b/packages/fluentui/CHANGELOG.md @@ -76,6 +76,7 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm - Fix missing export for `AttachmentBodyStylesProps` @ling1726 ([#16260](https://github.com/microsoft/fluentui/pull/16260)) - Fix throwing error when using `ChatMessage` with children without declaring the `header` prop @ling1726 ([#16321](https://github.com/microsoft/fluentui/pull/16321)) - Fix throwing error in `fontSizeUtility` on CodeSandbox @layershifter ([#16368](https://github.com/microsoft/fluentui/pull/16368)) +- Fix implementation of getReactFiberFromNode to be compatible with React 17 @layershifter ([#16392](https://github.com/microsoft/fluentui/pull/16392)) ### Features - Add 2.0 light and dark themes @jurokapsiar ([#15867](https://github.com/microsoft/fluentui/pull/15867)) From 6f35a7540d82d7c7d0378e98342b381c9436b595 Mon Sep 17 00:00:00 2001 From: Oleksandr Fediashov Date: Wed, 6 Jan 2021 17:34:43 +0100 Subject: [PATCH 3/3] add a UT --- .../test/specs/utils/getReactFiberFromNode-test.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx b/packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx index b881b9c8a6864..4d18a81bf367c 100644 --- a/packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx +++ b/packages/fluentui/react-northstar/test/specs/utils/getReactFiberFromNode-test.tsx @@ -23,6 +23,12 @@ describe('getReactFiberFromNode', () => { document.body.removeChild(rootEl); }); + it('throws on non-React node', () => { + expect(() => getReactFiberFromNode(document.createElement('div'))).toThrow( + /Failed to find a React Fiber on a node/, + ); + }); + it('returns null if nothing was passed', () => { expect(getReactFiberFromNode(undefined)).toBe(null); });