fix(tabs, tab-bar): use standalone tab bar in Vue, React (#29940)

Issue number: resolves #29885, resolves #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
https://github.com/ionic-team/ionic-framework/pull/29925 and PR
https://github.com/ionic-team/ionic-framework/pull/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
This commit is contained in:
Maria Hutt
2024-10-16 11:08:54 -07:00
committed by GitHub
parent cdb4456be2
commit b7b383bee0
5 changed files with 175 additions and 135 deletions

View File

@ -1,5 +1,5 @@
import { defineCustomElement } from "@ionic/core/components/ion-tab-bar.js";
import type { VNode } from "vue";
import type { VNode, Ref } from "vue";
import { h, defineComponent, getCurrentInstance, inject } from "vue";
// TODO(FW-2969): types
@ -16,6 +16,12 @@ interface Tab {
ref: VNode;
}
interface TabBarData {
hasRouterOutlet: boolean;
_tabsWillChange: Function;
_tabsDidChange: Function;
}
const isTabButton = (child: any) => child.type?.name === "IonTabButton";
const getTabs = (nodes: VNode[]) => {
@ -34,20 +40,23 @@ const getTabs = (nodes: VNode[]) => {
export const IonTabBar = defineComponent({
name: "IonTabBar",
props: {
/* eslint-disable @typescript-eslint/no-empty-function */
_tabsWillChange: { type: Function, default: () => {} },
_tabsDidChange: { type: Function, default: () => {} },
_hasRouterOutlet: { type: Boolean, default: false },
/* eslint-enable @typescript-eslint/no-empty-function */
},
data() {
return {
tabState: {
activeTab: undefined,
tabs: {},
/**
* Passing this prop to each tab button
* lets it be aware of the presence of
* the router outlet.
*/
hasRouterOutlet: false,
},
tabVnodes: [],
/* eslint-disable @typescript-eslint/no-empty-function */
_tabsWillChange: { type: Function, default: () => {} },
_tabsDidChange: { type: Function, default: () => {} },
/* eslint-enable @typescript-eslint/no-empty-function */
};
},
updated() {
@ -55,7 +64,7 @@ export const IonTabBar = defineComponent({
},
methods: {
setupTabState(ionRouter: any) {
const hasRouterOutlet = this.$props._hasRouterOutlet;
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
/**
* For each tab, we need to keep track of its
* base href as well as any child page that
@ -75,13 +84,6 @@ export const IonTabBar = defineComponent({
ref: child,
};
/**
* Passing this prop to each tab button
* lets it be aware of the presence of
* the router outlet.
*/
tabState.hasRouterOutlet = hasRouterOutlet;
/**
* Passing this prop to each tab button
* lets it be aware of the state that
@ -126,7 +128,7 @@ export const IonTabBar = defineComponent({
* @param ionRouter
*/
checkActiveTab(ionRouter: any) {
const hasRouterOutlet = this.$props._hasRouterOutlet;
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
const currentRoute = ionRouter?.getCurrentRouteInfo();
const childNodes = this.$data.tabVnodes;
const { tabs, activeTab: prevActiveTab } = this.$data.tabState;
@ -216,7 +218,7 @@ export const IonTabBar = defineComponent({
this.tabSwitch(activeTab);
},
tabSwitch(activeTab: string, ionRouter?: any) {
const hasRouterOutlet = this.$props._hasRouterOutlet;
const hasRouterOutlet = this.$data.tabState.hasRouterOutlet;
const childNodes = this.$data.tabVnodes;
const { activeTab: prevActiveTab } = this.$data.tabState;
const tabState = this.$data.tabState;
@ -227,7 +229,7 @@ export const IonTabBar = defineComponent({
const tabDidChange = activeTab !== prevActiveTab;
if (tabBar) {
if (activeChild) {
tabDidChange && this.$props._tabsWillChange(activeTab);
tabDidChange && this.$data._tabsWillChange(activeTab);
if (hasRouterOutlet && ionRouter !== null) {
ionRouter.handleSetCurrentTab(activeTab);
@ -235,7 +237,7 @@ export const IonTabBar = defineComponent({
tabBar.selectedTab = tabState.activeTab = activeTab;
tabDidChange && this.$props._tabsDidChange(activeTab);
tabDidChange && this.$data._tabsDidChange(activeTab);
} else {
/**
* When going to a tab that does
@ -250,6 +252,17 @@ export const IonTabBar = defineComponent({
},
mounted() {
const ionRouter: any = inject("navManager", null);
/**
* Tab bar can be used as a standalone component,
* so it cannot be modified directly through
* IonTabs. Instead, data will be passed through
* the provide/inject.
*/
const tabBarData = inject<Ref<TabBarData>>("tabBarData");
this.$data.tabState.hasRouterOutlet = tabBarData.value.hasRouterOutlet;
this.$data._tabsWillChange = tabBarData.value._tabsWillChange;
this.$data._tabsDidChange = tabBarData.value._tabsDidChange;
this.setupTabState(ionRouter);

View File

@ -1,6 +1,13 @@
import { defineCustomElement } from "@ionic/core/components/ion-tabs.js";
import type { VNode } from "vue";
import { h, defineComponent, Fragment, isVNode } from "vue";
import {
h,
defineComponent,
Fragment,
isVNode,
provide,
shallowRef,
} from "vue";
import { IonTab } from "../proxies";
@ -9,6 +16,12 @@ const DID_CHANGE = "ionTabsDidChange";
// TODO(FW-2969): types
interface TabBarData {
hasRouterOutlet: boolean;
_tabsWillChange: Function;
_tabsDidChange: Function;
}
/**
* Vue 3.2.38 fixed an issue where Web Component
* names are respected using kebab case instead of pascal case.
@ -24,13 +37,6 @@ const isRouterOutlet = (node: VNode) => {
);
};
const isTabBar = (node: VNode) => {
return (
node.type &&
((node.type as any).name === "IonTabBar" || node.type === "ion-tab-bar")
);
};
const isTab = (node: VNode): boolean => {
// The `ion-tab` component was created with the `v-for` directive.
if (node.type === Fragment) {
@ -49,7 +55,43 @@ const isTab = (node: VNode): boolean => {
export const IonTabs = /*@__PURE__*/ defineComponent({
name: "IonTabs",
emits: [WILL_CHANGE, DID_CHANGE],
data() {
return {
hasRouterOutlet: false,
};
},
setup(props, { slots, emit }) {
const slottedContent: VNode[] | undefined =
slots.default && slots.default();
let routerOutlet: VNode | undefined = undefined;
if (slottedContent && slottedContent.length > 0) {
/**
* Developers must pass an ion-router-outlet
* inside of ion-tabs if they want to use
* the history stack or URL updates associated
* with the router.
*/
routerOutlet = slottedContent.find((child: VNode) =>
isRouterOutlet(child)
);
}
/**
* Tab bar can be used as a standalone component,
* so it cannot be modified directly through
* IonTabs. Instead, data will be passed through
* the provide/inject.
*/
provide(
"tabBarData",
shallowRef<TabBarData>({
hasRouterOutlet: !!routerOutlet,
_tabsWillChange: (tab: string) => emit(WILL_CHANGE, { tab }),
_tabsDidChange: (tab: string) => emit(DID_CHANGE, { tab }),
})
);
return {
props,
slots,
@ -68,9 +110,10 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
defineCustomElement();
},
render() {
const { slots, emit, props } = this;
const slottedContent = slots.default && slots.default();
let routerOutlet;
const { slots, props } = this;
const slottedContent: VNode[] | undefined =
slots.default && slots.default();
let routerOutlet: VNode | undefined = undefined;
let hasTab = false;
if (slottedContent && slottedContent.length > 0) {
@ -78,7 +121,7 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
* Developers must pass an ion-router-outlet
* inside of ion-tabs if they want to use
* the history stack or URL updates associated
* wit the router.
* with the router.
*/
routerOutlet = slottedContent.find((child: VNode) =>
isRouterOutlet(child)
@ -103,30 +146,6 @@ export const IonTabs = /*@__PURE__*/ defineComponent({
);
}
if (slottedContent && slottedContent.length > 0) {
const slottedTabBar = slottedContent.find((child: VNode) =>
isTabBar(child)
);
if (slottedTabBar) {
if (!slottedTabBar.props) {
slottedTabBar.props = {};
}
/**
* ionTabsWillChange and ionTabsDidChange are
* fired from `ion-tabs`, so we need to pass these down
* as props so they can fire when the active tab changes.
* TODO: We may want to move logic from the tab bar into here
* so we do not have code split across two components.
*/
slottedTabBar.props._tabsWillChange = (tab: string) =>
emit(WILL_CHANGE, { tab });
slottedTabBar.props._tabsDidChange = (tab: string) =>
emit(DID_CHANGE, { tab });
slottedTabBar.props._hasRouterOutlet = !!routerOutlet;
}
}
if (hasTab) {
return h(
"ion-tabs",