mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
fix(react): callback refs now work correctly with ionic components (#23152)
resolves #23153
This commit is contained in:
committed by
GitHub
parent
004885bfd4
commit
0dd189e2c0
@@ -22,7 +22,7 @@ interface IonIconProps {
|
||||
}
|
||||
|
||||
type InternalProps = IonIconProps & {
|
||||
forwardedRef?: React.RefObject<HTMLIonIconElement>;
|
||||
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
|
||||
};
|
||||
|
||||
class IonIconContainer extends React.PureComponent<InternalProps> {
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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__*/ (() =>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import { StackContext } from './StackContext';
|
||||
|
||||
interface OutletPageManagerProps {
|
||||
className?: string;
|
||||
forwardedRef?: React.RefObject<HTMLIonRouterOutletElement>;
|
||||
forwardedRef?: React.ForwardedRef<HTMLIonRouterOutletElement>;
|
||||
routeInfo?: RouteInfo;
|
||||
StackManager: any;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user