fix(react): callback refs now work correctly with ionic components (#23152)

resolves #23153
This commit is contained in:
Tucker Whitehouse
2021-04-22 09:56:04 -04:00
committed by GitHub
parent 004885bfd4
commit 0dd189e2c0
14 changed files with 63 additions and 38 deletions

View File

@@ -22,7 +22,7 @@ interface IonIconProps {
}
type InternalProps = IonIconProps & {
forwardedRef?: React.RefObject<HTMLIonIconElement>;
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
};
class IonIconContainer extends React.PureComponent<InternalProps> {

View File

@@ -9,7 +9,7 @@ import { createForwardRef } from './utils';
interface IonPageProps extends IonicReactProps {}
interface IonPageInternalProps extends IonPageProps {
forwardedRef?: React.RefObject<HTMLDivElement>;
forwardedRef?: React.ForwardedRef<HTMLDivElement>;
}
class IonPageInternal extends React.Component<IonPageInternalProps> {

View File

@@ -10,12 +10,12 @@ import { createForwardRef } from './utils';
type Props = LocalJSX.IonRouterOutlet & {
basePath?: string;
ref?: React.RefObject<any>;
ref?: React.Ref<any>;
ionPage?: boolean;
};
interface InternalProps extends Props {
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
forwardedRef?: React.ForwardedRef<HTMLIonRouterOutletElement>;
}
interface InternalState {}

View File

@@ -27,7 +27,7 @@ describe('createComponent - events', () => {
});
describe('createComponent - ref', () => {
test('should pass ref on to web component instance', () => {
test('should pass ref on to web component instance (RefObject)', () => {
const ionButtonRef: React.RefObject<any> = React.createRef();
const IonButton = createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button');
@@ -35,6 +35,16 @@ describe('createComponent - ref', () => {
const ionButtonItem = getByText('ButtonNameA');
expect(ionButtonRef.current).toEqual(ionButtonItem);
});
test('should pass ref on to web component instance (RefCallback)', () => {
let current
const ionButtonRef: React.RefCallback<any> = value => current = value;
const IonButton = createReactComponent<JSX.IonButton, HTMLIonButtonElement>('ion-button');
const { getByText } = render(<IonButton ref={ionButtonRef}>ButtonNameA</IonButton>);
const ionButtonItem = getByText('ButtonNameA');
expect(current).toEqual(ionButtonItem);
});
});
describe('createComponent - strict mode', () => {

View File

@@ -11,10 +11,11 @@ import {
createForwardRef,
dashToPascalCase,
isCoveredByReact,
mergeRefs,
} from './utils';
interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
forwardedRef?: React.RefObject<ElementType>;
forwardedRef?: React.ForwardedRef<ElementType>;
href?: string;
routerLink?: string;
ref?: React.Ref<any>;
@@ -31,12 +32,14 @@ export const createReactComponent = <PropType, ElementType>(
const ReactComponent = class extends React.Component<IonicReactInternalProps<PropType>> {
context!: React.ContextType<typeof NavContext>;
ref: React.RefObject<HTMLElement>;
stableMergedRefs: React.RefCallback<HTMLElement>
constructor(props: IonicReactInternalProps<PropType>) {
super(props);
// If we weren't given a ref to forward, we still need one
// in order to attach props to the wrapped element.
// Create a local ref to to attach props to the wrapped element.
this.ref = React.createRef();
// React refs must be stable (not created inline).
this.stableMergedRefs = mergeRefs(this.ref, this.props.forwardedRef)
}
componentDidMount() {
@@ -44,9 +47,7 @@ export const createReactComponent = <PropType, ElementType>(
}
componentDidUpdate(prevProps: IonicReactInternalProps<PropType>) {
// Try to use the forwarded ref to get the child node.
// Otherwise, use the one we created.
const node = (this.props.forwardedRef?.current || this.ref.current!) as HTMLElement;
const node = this.ref.current! as HTMLElement;
attachProps(node, this.props, prevProps);
}
@@ -81,7 +82,7 @@ export const createReactComponent = <PropType, ElementType>(
const newProps: IonicReactInternalProps<PropType> = {
...propsToPass,
ref: forwardedRef || this.ref,
ref: this.stableMergedRefs,
style,
};

View File

@@ -1,7 +1,7 @@
import { OverlayEventDetail } from '@ionic/core';
import React from 'react';
import { attachProps } from './utils';
import { attachProps, setRef } from './utils';
interface OverlayBase extends HTMLElement {
present: () => Promise<void>;
@@ -30,7 +30,7 @@ export const createControllerComponent = <
type Props = OptionsType &
ReactControllerProps & {
forwardedRef?: React.RefObject<OverlayType>;
forwardedRef?: React.ForwardedRef<OverlayType>;
};
class Overlay extends React.Component<Props> {
@@ -73,9 +73,7 @@ export const createControllerComponent = <
if (this.props.onDidDismiss) {
this.props.onDidDismiss(event);
}
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = undefined;
}
setRef(this.props.forwardedRef, null)
}
async present(prevProps?: Props) {
@@ -106,9 +104,7 @@ export const createControllerComponent = <
// Check isOpen again since the value could have changed during the async call to controller.create
// It's also possible for the component to have become unmounted.
if (this.props.isOpen === true && this.isUnmounted === false) {
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = this.overlay;
}
setRef(this.props.forwardedRef, this.overlay)
await this.overlay.present();
}
}

View File

@@ -2,7 +2,7 @@ import { OverlayEventDetail } from '@ionic/core';
import React from 'react';
import ReactDOM from 'react-dom';
import { attachProps } from './utils';
import { attachProps, setRef } from './utils';
interface OverlayElement extends HTMLElement {
present: () => Promise<void>;
@@ -32,7 +32,7 @@ export const createOverlayComponent = <
type Props = OverlayComponent &
ReactOverlayProps & {
forwardedRef?: React.RefObject<OverlayType>;
forwardedRef?: React.ForwardedRef<OverlayType>;
};
let isDismissing = false;
@@ -69,9 +69,7 @@ export const createOverlayComponent = <
if (this.props.onDidDismiss) {
this.props.onDidDismiss(event);
}
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = undefined;
}
setRef(this.props.forwardedRef, null)
}
shouldComponentUpdate(nextProps: Props) {
@@ -132,10 +130,7 @@ export const createOverlayComponent = <
componentProps: {},
});
if (this.props.forwardedRef) {
(this.props.forwardedRef as any).current = this.overlay;
}
setRef(this.props.forwardedRef, this.overlay);
attachProps(this.overlay, elementProps, prevProps);
await this.overlay.present();

View File

@@ -18,7 +18,7 @@ export const IonBackButtonInner = /*@__PURE__*/ createReactComponent<
export const IonRouterOutletInner = /*@__PURE__*/ createReactComponent<
JSX.IonRouterOutlet & {
setRef?: (val: HTMLIonRouterOutletElement) => void;
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
forwardedRef?: React.ForwardedRef<HTMLIonRouterOutletElement>;
},
HTMLIonRouterOutletElement
>('ion-router-outlet');

View File

@@ -13,7 +13,7 @@ type Props = Omit<LocalJSX.IonBackButton, 'icon'> &
md: string;
}
| string;
ref?: React.RefObject<HTMLIonBackButtonElement>;
ref?: React.Ref<HTMLIonBackButtonElement>;
};
export const IonBackButton = /*@__PURE__*/ (() =>

View File

@@ -18,7 +18,7 @@ type IonTabBarProps = LocalJSX.IonTabBar &
};
interface InternalProps extends IonTabBarProps {
forwardedRef?: React.RefObject<HTMLIonIconElement>;
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
routeInfo: RouteInfo;
}

View File

@@ -8,7 +8,7 @@ import { IonTabButtonInner } from '../inner-proxies';
type Props = LocalJSX.IonTabButton &
IonicReactProps & {
routerOptions?: RouterOptions;
ref?: React.RefObject<HTMLIonTabButtonElement>;
ref?: React.Ref<HTMLIonTabButtonElement>;
onClick?: (e: any) => void;
};

View File

@@ -18,7 +18,7 @@ export const createForwardRef = <PropType, ElementType>(
) => {
const forwardRef = (
props: IonicReactExternalProps<PropType, ElementType>,
ref: React.Ref<ElementType>
ref: React.ForwardedRef<ElementType>
) => {
return <ReactComponent {...props} forwardedRef={ref} />;
};
@@ -27,6 +27,25 @@ export const createForwardRef = <PropType, ElementType>(
return React.forwardRef(forwardRef);
};
export const setRef = (ref: React.ForwardedRef<any> | React.Ref<any> | undefined, value: any) => {
if (typeof ref === 'function') {
ref(value)
} else if (ref != null) {
// Cast as a MutableRef so we can assign current
(ref as React.MutableRefObject<any>).current = value
}
};
export const mergeRefs = (
...refs: (React.ForwardedRef<any> | React.Ref<any> | undefined)[]
): React.RefCallback<any> => {
return (value: any) => {
refs.forEach(ref => {
setRef(ref, value)
})
}
};
export * from './attachProps';
export * from './case';

View File

@@ -8,7 +8,7 @@ import { StackContext } from './StackContext';
interface OutletPageManagerProps {
className?: string;
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
forwardedRef?: React.ForwardedRef<HTMLIonRouterOutletElement>;
routeInfo?: RouteInfo;
StackManager: any;
}

View File

@@ -1,5 +1,6 @@
import React from 'react';
import { mergeRefs } from '../components/utils';
import { IonLifeCycleContext } from '../contexts/IonLifeCycleContext';
import { RouteInfo } from '../models';
@@ -7,7 +8,7 @@ import { StackContext } from './StackContext';
interface PageManagerProps {
className?: string;
forwardedRef?: React.RefObject<HTMLDivElement>;
forwardedRef?: React.ForwardedRef<HTMLDivElement>;
routeInfo?: RouteInfo;
}
@@ -15,10 +16,13 @@ export class PageManager extends React.PureComponent<PageManagerProps> {
ionLifeCycleContext!: React.ContextType<typeof IonLifeCycleContext>;
context!: React.ContextType<typeof StackContext>;
ionPageElementRef: React.RefObject<HTMLDivElement>;
stableMergedRefs: React.RefCallback<HTMLDivElement>
constructor(props: PageManagerProps) {
super(props);
this.ionPageElementRef = this.props.forwardedRef || React.createRef();
this.ionPageElementRef = React.createRef();
// React refs must be stable (not created inline).
this.stableMergedRefs = mergeRefs(this.ionPageElementRef, this.props.forwardedRef)
}
componentDidMount() {
@@ -95,7 +99,7 @@ export class PageManager extends React.PureComponent<PageManagerProps> {
className={
className ? `${className} ion-page` : `ion-page`
}
ref={this.ionPageElementRef}
ref={this.stableMergedRefs}
{...props}
>
{children}