Skip to content

Commit ef16a63

Browse files
committed
refactor(tabs, tab-bar): cleanup context
1 parent f323d69 commit ef16a63

File tree

3 files changed

+73
-95
lines changed

3 files changed

+73
-95
lines changed

packages/react/src/components/navigation/IonTabBar.tsx

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { createForwardRef } from '../utils';
99

1010
import { IonTabButton } from './IonTabButton';
1111
import { IonTabsContext } from './IonTabsContext';
12+
import type { IonTabsContextState } from './IonTabsContext';
1213

1314
type IonTabBarProps = LocalJSX.IonTabBar &
1415
IonicReactProps & {
@@ -22,18 +23,7 @@ interface InternalProps extends IonTabBarProps {
2223
forwardedRef?: React.ForwardedRef<HTMLIonIconElement>;
2324
onSetCurrentTab: (tab: string, routeInfo: RouteInfo) => void;
2425
routeInfo: RouteInfo;
25-
/**
26-
* This prop is set by the `IonTabs` component. If
27-
* the value is `undefined`, then the `ion-tab-bar`
28-
* component was not found within the slotted content.
29-
* Most likely, the tab bar was not passed as a direct
30-
* child of `IonTabs`.
31-
*
32-
* A workaround will be used to determine if the tabs
33-
* are being used as a basic tab navigation or with
34-
* the router.
35-
*/
36-
hasRouterOutlet: boolean;
26+
tabsContext?: IonTabsContextState;
3727
}
3828

3929
interface TabUrls {
@@ -52,12 +42,10 @@ interface IonTabBarState {
5242

5343
class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarState> {
5444
context!: React.ContextType<typeof NavContext>;
55-
private tabBarRef: React.RefObject<HTMLIonTabBarElement>;
5645

5746
constructor(props: InternalProps) {
5847
super(props);
5948
const tabs: { [key: string]: TabUrls } = {};
60-
this.tabBarRef = React.createRef<HTMLIonTabBarElement>();
6149
React.Children.forEach((props as any).children, (child: any) => {
6250
if (
6351
child != null &&
@@ -197,13 +185,14 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
197185
) {
198186
const tappedTab = this.state.tabs[e.detail.tab];
199187
const originalHref = tappedTab.originalHref;
188+
const hasRouterOutlet = this.props.tabsContext?.hasRouterOutlet;
200189

201190
/**
202191
* If the router outlet is not defined, then the tabs is being used
203192
* as a basic tab navigation without the router. In this case, we
204193
* don't want to update the href else the URL will change.
205194
*/
206-
const currentHref = this.props.hasRouterOutlet ? e.detail.href : '';
195+
const currentHref = hasRouterOutlet ? e.detail.href : '';
207196
const { activeTab: prevActiveTab } = this.state;
208197

209198
if (onClickFn) {
@@ -227,7 +216,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
227216
if (this.props.onIonTabsDidChange) {
228217
this.props.onIonTabsDidChange(new CustomEvent('ionTabDidChange', { detail: { tab: e.detail.tab } }));
229218
}
230-
if (this.props.hasRouterOutlet) {
219+
if (hasRouterOutlet) {
231220
this.setActiveTabOnContext(e.detail.tab);
232221
this.context.changeTab(e.detail.tab, currentHref, e.detail.routeOptions);
233222
}
@@ -264,7 +253,7 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
264253
render() {
265254
const { activeTab } = this.state;
266255
return (
267-
<IonTabBarInner ref={this.tabBarRef} {...this.props} selectedTab={activeTab}>
256+
<IonTabBarInner {...this.props} selectedTab={activeTab}>
268257
{React.Children.map(this.props.children as any, this.renderTabButton(activeTab))}
269258
</IonTabBarInner>
270259
);
@@ -278,14 +267,28 @@ class IonTabBarUnwrapped extends React.PureComponent<InternalProps, IonTabBarSta
278267
const IonTabBarContainer: React.FC<InternalProps> = React.memo<InternalProps>(({ forwardedRef, ...props }) => {
279268
const context = useContext(NavContext);
280269
const tabsContext = useContext(IonTabsContext);
270+
const tabBarRef = forwardedRef || tabsContext.tabBarProps.ref;
271+
const updatedTabBarProps = {
272+
...tabsContext.tabBarProps,
273+
ref: tabBarRef,
274+
};
281275

282276
return (
283277
<IonTabBarUnwrapped
284-
ref={forwardedRef}
278+
ref={tabBarRef}
285279
{...(props as any)}
286280
routeInfo={props.routeInfo || context.routeInfo || { pathname: window.location.pathname }}
287281
onSetCurrentTab={context.setCurrentTab}
288-
hasRouterOutlet={tabsContext.hasRouterOutlet}
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+
}}
289292
>
290293
{props.children}
291294
</IonTabBarUnwrapped>

packages/react/src/components/navigation/IonTabs.tsx

Lines changed: 37 additions & 76 deletions
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,36 +42,23 @@ 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>;
68-
routerOutletRef: React.Ref<HTMLIonRouterOutletElement> = React.createRef();
6948
selectTabHandler?: (tag: string) => boolean;
7049
tabBarRef = React.createRef<any>();
7150

7251
ionTabContextState: IonTabsContextState = {
7352
activeTab: undefined,
7453
selectTab: () => false,
7554
hasRouterOutlet: false,
55+
/**
56+
* Tab bar can be used as a standalone component,
57+
* so the props can not be passed directly to the
58+
* tab bar component. Instead, props will be
59+
* passed through the context.
60+
*/
61+
tabBarProps: { ref: this.tabBarRef },
7662
};
7763

7864
constructor(props: Props) {
@@ -91,9 +77,32 @@ export const IonTabs = /*@__PURE__*/ (() =>
9177
}
9278
}
9379

80+
renderTabsInner(children: React.ReactNode, outlet: React.ReactElement<{}> | undefined) {
81+
return (
82+
<IonTabsInner {...this.props}>
83+
{React.Children.map(children, (child: React.ReactNode) => {
84+
if (React.isValidElement(child)) {
85+
const isRouterOutlet =
86+
child.type === IonRouterOutlet ||
87+
(child.type as any).isRouterOutlet ||
88+
(child.type === Fragment && child.props.children[0].type === IonRouterOutlet);
89+
90+
if (isRouterOutlet) {
91+
/**
92+
* The modified outlet needs to be returned to include
93+
* the ref.
94+
*/
95+
return outlet;
96+
}
97+
}
98+
return child;
99+
})}
100+
</IonTabsInner>
101+
);
102+
}
103+
94104
render() {
95105
let outlet: React.ReactElement<{}> | undefined;
96-
let tabBar: React.ReactElement | undefined;
97106
// Check if IonTabs has any IonTab children
98107
let hasTab = false;
99108
const { className, onIonTabsDidChange, onIonTabsWillChange, ...props } = this.props;
@@ -103,19 +112,15 @@ export const IonTabs = /*@__PURE__*/ (() =>
103112
? (this.props.children as ChildFunction)(this.ionTabContextState)
104113
: this.props.children;
105114

106-
const outletProps = {
107-
ref: this.routerOutletRef,
108-
};
109-
110115
React.Children.forEach(children, (child: any) => {
111116
// eslint-disable-next-line no-prototype-builtins
112117
if (child == null || typeof child !== 'object' || !child.hasOwnProperty('type')) {
113118
return;
114119
}
115120
if (child.type === IonRouterOutlet || child.type.isRouterOutlet) {
116-
outlet = React.cloneElement(child, outletProps);
121+
outlet = React.cloneElement(child);
117122
} else if (child.type === Fragment && child.props.children[0].type === IonRouterOutlet) {
118-
outlet = React.cloneElement(child.props.children[0], outletProps);
123+
outlet = React.cloneElement(child.props.children[0]);
119124
} else if (child.type === IonTab) {
120125
/**
121126
* This indicates that IonTabs will be using a basic tab-based navigation
@@ -127,8 +132,7 @@ export const IonTabs = /*@__PURE__*/ (() =>
127132
this.ionTabContextState.hasRouterOutlet = !!outlet;
128133

129134
let childProps: any = {
130-
ref: this.tabBarRef,
131-
routerOutletRef: this.routerOutletRef,
135+
...this.ionTabContextState.tabBarProps,
132136
};
133137

134138
/**
@@ -152,14 +156,7 @@ export const IonTabs = /*@__PURE__*/ (() =>
152156
};
153157
}
154158

155-
if (child.type === IonTabBar || child.type.isTabBar) {
156-
tabBar = React.cloneElement(child, childProps);
157-
} else if (
158-
child.type === Fragment &&
159-
(child.props.children[1].type === IonTabBar || child.props.children[1].type.isTabBar)
160-
) {
161-
tabBar = React.cloneElement(child.props.children[1], childProps);
162-
}
159+
this.ionTabContextState.tabBarProps = childProps;
163160
});
164161

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

packages/react/src/components/navigation/IonTabsContext.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,24 @@ export interface IonTabsContextState {
44
activeTab: string | undefined;
55
selectTab: (tab: string) => boolean;
66
hasRouterOutlet: boolean;
7+
tabBarProps: TabBarProps;
78
}
89

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+
922
export const IonTabsContext = React.createContext<IonTabsContextState>({
1023
activeTab: undefined,
1124
selectTab: () => false,
1225
hasRouterOutlet: false,
26+
tabBarProps: { ref: React.createRef() },
1327
});

0 commit comments

Comments
 (0)