Skip to content

Commit b7b383b

Browse files
authored
fix(tabs, tab-bar): use standalone tab bar in Vue, React (ionic-team#29940)
Issue number: resolves ionic-team#29885, resolves ionic-team#29924 --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> React and Vue: Tab bar could be a standalone element within `IonTabs` and would navigate without issues with a router outlet before v8.3: ```tsx <IonTabs> <IonRouterOutlet></IonRouterOutlet> <IonTabBar></IonTabBar> </IonTabs> ``` It would work as if it was written as: ```tsx <IonTabs> <IonRouterOutlet></IonRouterOutlet> <IonTabBar slot="bottom"> <!-- Buttons --> </IonTabBar> </IonTabs> ``` After v8.3, any `ion-tab-bar` that was not a direct child of `ion-tabs` would lose it's expected behavior when used with a router outlet. If a user clicked on a tab button, then the content would not be redirected to that expected view. React only: Users can no longer add a `ref` to the `IonRouterOutlet`, it always returns undefined. ``` <IonTabs> <IonRouterOutlet ref={ref}> <IonTabBar slot="bottom"> <!-- Buttons --> </IonTabBar> </IonTabs> ``` ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> The fixes were already reviewed through PR ionic-team#29925 and PR ionic-team#29927. I split them to make it easier to review. React and Vue: The React tabs has been updated to pass data to the tab bar through context instead of passing it through a ref. By using a context, the data will be available for the tab bar to use regardless of its level. React only: Reverted the logic for `routerOutletRef` and added a comment of the importance of it. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> N/A
1 parent cdb4456 commit b7b383b

File tree

5 files changed

+175
-135
lines changed

5 files changed

+175
-135
lines changed

Diff for: packages/react/src/components/navigation/IonTabBar.tsx

+25-4
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import { IonTabBarInner } from '../inner-proxies';
88
import { createForwardRef } from '../utils';
99

1010
import { IonTabButton } from './IonTabButton';
11+
import { IonTabsContext } from './IonTabsContext';
12+
import type { IonTabsContextState } from './IonTabsContext';
1113

1214
type IonTabBarProps = LocalJSX.IonTabBar &
1315
IonicReactProps & {
@@ -21,7 +23,7 @@ interface InternalProps extends IonTabBarProps {
2123
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
2224
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
2325
routeInfo: RouteInfo;
24-
routerOutletRef?: React.RefObject<HTMLIonRouterOutletElement> | undefined;
26+
tabsContext?: IonTabsContextState;
2527
}
2628

2729
interface TabUrls {
@@ -183,12 +185,14 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
183185
) {
184186
const tappedTab = this.state.tabs[e.detail.tab];
185187
const originalHref = tappedTab.originalHref;
188+
const hasRouterOutlet = this.props.tabsContext?.hasRouterOutlet;
189+
186190
/**
187191
* If the router outlet is not defined, then the tabs is being used
188192
* as a basic tab navigation without the router. In this case, we
189193
* don't want to update the href else the URL will change.
190194
*/
191-
const currentHref = this.props.routerOutletRef?.current ? e.detail.href : '';
195+
const currentHref = hasRouterOutlet ? e.detail.href : '';
192196
const { activeTab: prevActiveTab } = this.state;
193197

194198
if (onClickFn) {
@@ -212,7 +216,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
212216
if (this.props.onIonTabsDidChange) {
213217
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
214218
}
215-
if (this.props.routerOutletRef?.current) {
219+
if (hasRouterOutlet) {
216220
this.setActiveTabOnContext(e.detail.tab);
217221
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
218222
}
@@ -262,12 +266,29 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
262266

263267
const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({ forwardedRef, ...props }) => {
264268
const context = useContext(NavContext);
269+
const tabsContext = useContext(IonTabsContext);
270+
const tabBarRef = forwardedRef || tabsContext.tabBarProps.ref;
271+
const updatedTabBarProps = {
272+
...tabsContext.tabBarProps,
273+
ref: tabBarRef,
274+
};
275+
265276
return (
266277
<IonTabBarUnwrapped
267-
ref={forwardedRef}
278+
ref={tabBarRef}
268279
{...(props as any)}
269280
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
270281
onSetCurrentTab={context.setCurrentTab}
282+
/**
283+
* Tab bar can be used as a standalone component,
284+
* so it cannot be modified directly through
285+
* IonTabs. Instead, props will be passed through
286+
* the context.
287+
*/
288+
tabsContext={{
289+
...tabsContext,
290+
tabBarProps: updatedTabBarProps,
291+
}}
271292
>
272293
{props.children}
273294
</IonTabBarUnwrapped>

Diff for: packages/react/src/components/navigation/IonTabs.tsx

+46-75
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { IonRouterOutlet } from '../IonRouterOutlet';
88
import { IonTabsInner } from '../inner-proxies';
99
import { IonTab } from '../proxies';
1010

11-
import { IonTabBar } from './IonTabBar';
1211
import type { IonTabsContextState } from './IonTabsContext';
1312
import { IonTabsContext } from './IonTabsContext';
1413

@@ -43,35 +42,30 @@ interface Props extends LocalJSX.IonTabs {
4342
children: ChildFunction | React.ReactNode;
4443
}
4544

46-
const hostStyles: React.CSSProperties = {
47-
display: 'flex',
48-
position: 'absolute',
49-
top: '0',
50-
left: '0',
51-
right: '0',
52-
bottom: '0',
53-
flexDirection: 'column',
54-
width: '100%',
55-
height: '100%',
56-
contain: 'layout size style',
57-
};
58-
59-
const tabsInner: React.CSSProperties = {
60-
position: 'relative',
61-
flex: 1,
62-
contain: 'layout size style',
63-
};
64-
6545
export const IonTabs = /*@__PURE__*/ (() =>
6646
class extends React.Component<Props> {
6747
context!: React.ContextType<typeof NavContext>;
48+
/**
49+
* `routerOutletRef` allows users to add a `ref` to `IonRouterOutlet`.
50+
* Without this, `ref.current` will be `undefined` in the user's app,
51+
* breaking their ability to access the `IonRouterOutlet` instance.
52+
* Do not remove this ref.
53+
*/
6854
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
6955
selectTabHandler?: (tag: string) => boolean;
7056
tabBarRef = React.createRef<any>();
7157

7258
ionTabContextState: IonTabsContextState = {
7359
activeTab: undefined,
7460
selectTab: () => false,
61+
hasRouterOutlet: false,
62+
/**
63+
* Tab bar can be used as a standalone component,
64+
* so the props can not be passed directly to the
65+
* tab bar component. Instead, props will be
66+
* passed through the context.
67+
*/
68+
tabBarProps: { ref: this.tabBarRef },
7569
};
7670

7771
constructor(props: Props) {
@@ -90,9 +84,32 @@ export const IonTabs = /*@__PURE__*/ (() =>
9084
}
9185
}
9286

87+
renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) {
88+
return (
89+
<IonTabsInner {...this.props}>
90+
{React.Children.map(children, (child: React.ReactNode) => {
91+
if (React.isValidElement(child)) {
92+
const isRouterOutlet =
93+
child.type === IonRouterOutlet ||
94+
(child.type as any).isRouterOutlet ||
95+
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
96+
97+
if (isRouterOutlet) {
98+
/**
99+
* The modified outlet needs to be returned to include
100+
* the ref.
101+
*/
102+
return outlet;
103+
}
104+
}
105+
return child;
106+
})}
107+
</IonTabsInner>
108+
);
109+
}
110+
93111
render() {
94112
let outlet: React.ReactElement<{}> | undefined;
95-
let tabBar: React.ReactElement | undefined;
96113
// Check if IonTabs has any IonTab children
97114
let hasTab = false;
98115
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
@@ -102,19 +119,15 @@ export const IonTabs = /*@__PURE__*/ (() =>
102119
? (this.props.children as ChildFunction)(this.ionTabContextState)
103120
: this.props.children;
104121

105-
const outletProps = {
106-
ref: this.routerOutletRef,
107-
};
108-
109122
React.Children.forEach(children, (child: any) => {
110123
// eslint-disable-next-line no-prototype-builtins
111124
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
112125
return;
113126
}
114127
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
115-
outlet = React.cloneElement(child, outletProps);
128+
outlet = React.cloneElement(child);
116129
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
117-
outlet = React.cloneElement(child.props.children[0], outletProps);
130+
outlet = React.cloneElement(child.props.children[0]);
118131
} else if (child.type === IonTab) {
119132
/**
120133
* This indicates that IonTabs will be using a basic tab-based navigation
@@ -123,9 +136,10 @@ export const IonTabs = /*@__PURE__*/ (() =>
123136
hasTab = true;
124137
}
125138

139+
this.ionTabContextState.hasRouterOutlet = !!outlet;
140+
126141
let childProps: any = {
127-
ref: this.tabBarRef,
128-
routerOutletRef: this.routerOutletRef,
142+
...this.ionTabContextState.tabBarProps,
129143
};
130144

131145
/**
@@ -149,14 +163,7 @@ export const IonTabs = /*@__PURE__*/ (() =>
149163
};
150164
}
151165

152-
if (child.type === IonTabBar || child.type.isTabBar) {
153-
tabBar = React.cloneElement(child, childProps);
154-
} else if (
155-
child.type === Fragment &&
156-
(child.props.children[1].type === IonTabBar || child.props.children[1].type.isTabBar)
157-
) {
158-
tabBar = React.cloneElement(child.props.children[1], childProps);
159-
}
166+
this.ionTabContextState.tabBarProps = childProps;
160167
});
161168

162169
if (!outlet && !hasTab) {
@@ -186,46 +193,10 @@ export const IonTabs = /*@__PURE__*/ (() =>
186193
<IonTabsContext.Provider value={this.ionTabContextState}>
187194
{this.context.hasIonicRouter() ? (
188195
<PageManager className={className ? `${className}` : ''} routeInfo={this.context.routeInfo} {...props}>
189-
<IonTabsInner {...this.props}>
190-
{React.Children.map(children, (child: React.ReactNode) => {
191-
if (React.isValidElement(child)) {
192-
const isTabBar =
193-
child.type === IonTabBar ||
194-
(child.type as any).isTabBar ||
195-
(child.type === Fragment &&
196-
(child.props.children[1].type === IonTabBar || child.props.children[1].type.isTabBar));
197-
const isRouterOutlet =
198-
child.type === IonRouterOutlet ||
199-
(child.type as any).isRouterOutlet ||
200-
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
201-
202-
if (isTabBar) {
203-
/**
204-
* The modified tabBar needs to be returned to include
205-
* the context and the overridden methods.
206-
*/
207-
return tabBar;
208-
}
209-
if (isRouterOutlet) {
210-
/**
211-
* The modified outlet needs to be returned to include
212-
* the ref.
213-
*/
214-
return outlet;
215-
}
216-
}
217-
return child;
218-
})}
219-
</IonTabsInner>
196+
{this.renderTabsInner(children, outlet)}
220197
</PageManager>
221198
) : (
222-
<div className={className ? `${className}` : 'ion-tabs'} {...props} style={hostStyles}>
223-
{tabBar?.props.slot === 'top' ? tabBar : null}
224-
<div style={tabsInner} className="tabs-inner">
225-
{outlet}
226-
</div>
227-
{tabBar?.props.slot === 'bottom' ? tabBar : null}
228-
</div>
199+
this.renderTabsInner(children, outlet)
229200
)}
230201
</IonTabsContext.Provider>
231202
);

Diff for: packages/react/src/components/navigation/IonTabsContext.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,25 @@ import React from 'react';
33
export interface IonTabsContextState {
44
activeTab: string | undefined;
55
selectTab: (tab: string) => boolean;
6+
hasRouterOutlet: boolean;
7+
tabBarProps: TabBarProps;
68
}
79

10+
/**
11+
* Tab bar can be used as a standalone component,
12+
* so the props can not be passed directly to the
13+
* tab bar component. Instead, props will be
14+
* passed through the context.
15+
*/
16+
type TabBarProps = {
17+
ref: React.RefObject<any>;
18+
onIonTabsWillChange?: (e: CustomEvent) => void;
19+
onIonTabsDidChange?: (e: CustomEvent) => void;
20+
};
21+
822
export const IonTabsContext = React.createContext<IonTabsContextState>({
923
activeTab: undefined,
1024
selectTab: () => false,
25+
hasRouterOutlet: false,
26+
tabBarProps: { ref: React.createRef() },
1127
});

0 commit comments

Comments
 (0)