Skip to content

Commit 02678f3

Browse files
authored
fix(react, vue): inline modals apply ion-page class (#27481)
Issue number: resolves #27470 --------- <!-- 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. --> Passing multiple elements in to an inline modal causes `.ion-page` to not get set. This causes content to get pushed off the bottom of the modal equal to the height of the header. React has some special CSS that prevents this: https://github.com/ionic-team/ionic-framework/blob/eb2772c0ce623de70cce42b5d36e86cdbba7bafb/packages/react/src/components/createInlineOverlayComponent.tsx#L137-L140 However, I think this should be delegated to `.ion-page` instead so the behavior is consistent across frameworks. For example, Angular uses `.ion-page`: https://github.com/ionic-team/ionic-framework/blob/eb2772c0ce623de70cce42b5d36e86cdbba7bafb/angular/src/directives/overlays/modal.ts#L82 ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> - Inline overlays in Ionic React and Ionic Vue wrap child content in `.ion-delegate-host.ion-page`. - Removed the custom flex styles from Ionic React as `.ion-page` has its own styles. ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change, please describe the impact and migration path for existing applications below. --> ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> Revised Design Doc: https://github.com/ionic-team/ionic-framework-design-documents/pull/84
1 parent e114fe4 commit 02678f3

File tree

11 files changed

+107
-14
lines changed

11 files changed

+107
-14
lines changed

packages/react/src/components/IonModal.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ import { createInlineOverlayComponent } from './createInlineOverlayComponent';
55

66
export const IonModal = /*@__PURE__*/ createInlineOverlayComponent<JSX.IonModal, HTMLIonModalElement>(
77
'ion-modal',
8-
defineCustomElement
8+
defineCustomElement,
9+
true
910
);

packages/react/src/components/createInlineOverlayComponent.tsx

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,8 @@ interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<Elem
2929

3030
export const createInlineOverlayComponent = <PropType, ElementType>(
3131
tagName: string,
32-
defineCustomElement?: () => void
32+
defineCustomElement?: () => void,
33+
hasDelegateHost?: boolean
3334
) => {
3435
if (defineCustomElement) {
3536
defineCustomElement();
@@ -116,6 +117,18 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
116117
style,
117118
};
118119

120+
/**
121+
* Some overlays need `.ion-page` so content
122+
* takes up the full size of the parent overlay.
123+
*/
124+
const getWrapperClasses = () => {
125+
if (hasDelegateHost) {
126+
return `${DELEGATE_HOST} ion-page`;
127+
}
128+
129+
return DELEGATE_HOST;
130+
};
131+
119132
return createElement(
120133
'template',
121134
{},
@@ -132,14 +145,8 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
132145
? createElement(
133146
'div',
134147
{
135-
id: 'ion-react-wrapper',
136148
ref: this.wrapperRef,
137-
className: 'ion-delegate-host',
138-
style: {
139-
display: 'flex',
140-
flexDirection: 'column',
141-
height: '100%',
142-
},
149+
className: getWrapperClasses(),
143150
},
144151
children
145152
)
@@ -194,3 +201,5 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
194201
};
195202
return createForwardRef<PropType, ElementType>(ReactComponent, displayName);
196203
};
204+
205+
const DELEGATE_HOST = 'ion-delegate-host';

packages/react/test/base/src/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import IonModalConditionalSibling from './pages/overlay-components/IonModalCondi
3232
import IonModalConditional from './pages/overlay-components/IonModalConditional';
3333
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
3434
import IonPopoverNested from './pages/overlay-components/IonPopoverNested';
35+
import IonModalMultipleChildren from './pages/overlay-components/IonModalMultipleChildren';
3536

3637
setupIonicReact();
3738

@@ -52,6 +53,10 @@ const App: React.FC = () => (
5253
path="/overlay-components/modal-datetime-button"
5354
component={IonModalDatetimeButton}
5455
/>
56+
<Route
57+
path="/overlay-components/modal-multiple-children"
58+
component={IonModalMultipleChildren}
59+
/>
5560
<Route path="/keep-contents-mounted" component={KeepContentsMounted} />
5661
<Route path="/navigation" component={NavComponent} />
5762
<Route path="/tabs" component={Tabs} />
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { IonButton, IonContent, IonModal } from '@ionic/react';
2+
3+
/**
4+
* Test inline modal rendering when content lacks a single root node
5+
*/
6+
const IonModalMultipleChildren = () => {
7+
return (
8+
<IonContent>
9+
<IonButton id="show-modal">Show Modal</IonButton>
10+
<IonModal trigger="show-modal">
11+
<div className="child-content">Content A</div>
12+
<div className="child-content">Content B</div>
13+
</IonModal>
14+
</IonContent>
15+
);
16+
};
17+
18+
export default IonModalMultipleChildren;

packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,16 @@ describe('IonModal: conditional rendering', () => {
7676
});
7777

7878
});
79+
80+
describe('IonModal: multiple children', () => {
81+
it('should render a root .ion-page when passed multiple children', () => {
82+
cy.visit('/overlay-components/modal-multiple-children');
83+
84+
cy.get('ion-button#show-modal').click();
85+
86+
cy.get('ion-modal').should('be.visible');
87+
88+
cy.get('ion-modal .ion-page').should('have.length', 1);
89+
cy.get('ion-modal .ion-page .child-content').should('have.length', 2);
90+
});
91+
});

packages/vue/scripts/copy-overlays.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ function generateOverlays() {
2525
},
2626
{
2727
tag: 'ion-modal',
28-
name: 'IonModal'
28+
name: 'IonModal',
29+
hasDelegateHost: true
2930
},
3031
{
3132
tag: 'ion-popover',
@@ -44,8 +45,10 @@ function generateOverlays() {
4445

4546
componentImports.push(`import { defineCustomElement as ${defineCustomElementFn} } from '@ionic/core/components/${component.tag}.js'`);
4647

48+
const delegateHostString = component.hasDelegateHost ? ', true' : '';
49+
4750
componentDefinitions.push(`
48-
export const ${component.name} = /*@__PURE__*/ defineOverlayContainer<JSX.${component.name}>('${component.tag}', ${defineCustomElementFn}, [${props.join(', ')}]);
51+
export const ${component.name} = /*@__PURE__*/ defineOverlayContainer<JSX.${component.name}>('${component.tag}', ${defineCustomElementFn}, [${props.join(', ')}]${delegateHostString});
4952
`);
5053
});
5154

packages/vue/src/components/Overlays.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const IonPicker = /*@__PURE__*/ defineOverlayContainer<JSX.IonPicker>('io
2727

2828
export const IonToast = /*@__PURE__*/ defineOverlayContainer<JSX.IonToast>('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'translucent', 'trigger']);
2929

30-
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'trigger']);
30+
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'trigger'], true);
3131

3232
export const IonPopover = /*@__PURE__*/ defineOverlayContainer<JSX.IonPopover>('ion-popover', defineIonPopoverCustomElement, ['alignment', 'animated', 'arrow', 'backdropDismiss', 'component', 'componentProps', 'dismissOnSelect', 'enterAnimation', 'event', 'htmlAttributes', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'reference', 'showBackdrop', 'side', 'size', 'translucent', 'trigger', 'triggerAction']);
3333

packages/vue/src/vue-component-lib/overlays.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ export interface OverlayProps {
99
const EMPTY_PROP = Symbol();
1010
const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP };
1111

12-
export const defineOverlayContainer = <Props extends object>(name: string, defineCustomElement: () => void, componentProps: string[] = [], controller?: any) => {
12+
export const defineOverlayContainer = <Props extends object>(name: string, defineCustomElement: () => void, componentProps: string[] = [], hasDelegateHost?: boolean, controller?: any) => {
1313

1414
const createControllerComponent = () => {
1515
return defineComponent<Props & OverlayProps>((props, { slots, emit }) => {
@@ -162,10 +162,22 @@ export const defineOverlayContainer = <Props extends object>(name: string, defin
162162
}
163163
}
164164

165+
/**
166+
* Some overlays need a wrapper element so content
167+
* takes up the full size of the parent overlay.
168+
*/
169+
const renderChildren = () => {
170+
if (hasDelegateHost) {
171+
return h('div', { className: 'ion-delegate-host ion-page' }, slots);
172+
}
173+
174+
return slots;
175+
}
176+
165177
return h(
166178
name,
167179
{ ...restOfProps, ref: elementRef },
168-
(isOpen.value || restOfProps.keepContentsMounted) ? slots : undefined
180+
(isOpen.value || restOfProps.keepContentsMounted) ? renderChildren() : undefined
169181
)
170182
}
171183
});

packages/vue/test/base/src/router/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ const routes: Array<RouteRecordRaw> = [
2929
path: '/overlays',
3030
component: () => import('@/views/Overlays.vue')
3131
},
32+
{
33+
path: '/modal-multiple-children',
34+
component: () => import('@/views/ModalMultipleChildren.vue')
35+
},
3236
{
3337
path: '/keep-contents-mounted',
3438
component: () => import('@/views/OverlaysKeepContentsMounted.vue')
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<template>
2+
<ion-page data-pageid="modal-multiple-children">
3+
<ion-content class="ion-padding" :fullscreen="true">
4+
<ion-button id="show-modal">Show Modal</ion-button>
5+
6+
<ion-modal trigger="show-modal">
7+
<div class="child-content">Content A</div>
8+
<div class="child-content">Content B</div>
9+
</ion-modal>
10+
</ion-content>
11+
</ion-page>
12+
</template>
13+
14+
<script setup lang="ts">
15+
import { IonButton, IonContent, IonPage, IonModal } from '@ionic/vue';
16+
</script>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
describe('modal - multiple children', () => {
2+
it('should render a root .ion-page when passed multiple children', () => {
3+
cy.visit('/modal-multiple-children');
4+
5+
cy.get('ion-button#show-modal').click();
6+
7+
cy.get('ion-modal').should('be.visible');
8+
9+
cy.get('ion-modal .ion-page').should('have.length', 1);
10+
cy.get('ion-modal .ion-page .child-content').should('have.length', 2);
11+
});
12+
})

0 commit comments

Comments
 (0)