diff --git a/packages/react/src/components/IonModal.tsx b/packages/react/src/components/IonModal.tsx index 7a7bb07115..e133a469b1 100644 --- a/packages/react/src/components/IonModal.tsx +++ b/packages/react/src/components/IonModal.tsx @@ -5,5 +5,6 @@ import { createInlineOverlayComponent } from './createInlineOverlayComponent'; export const IonModal = /*@__PURE__*/ createInlineOverlayComponent( 'ion-modal', - defineCustomElement + defineCustomElement, + true ); diff --git a/packages/react/src/components/createInlineOverlayComponent.tsx b/packages/react/src/components/createInlineOverlayComponent.tsx index 70ea275222..38679e8515 100644 --- a/packages/react/src/components/createInlineOverlayComponent.tsx +++ b/packages/react/src/components/createInlineOverlayComponent.tsx @@ -29,7 +29,8 @@ interface IonicReactInternalProps extends React.HTMLAttributes( tagName: string, - defineCustomElement?: () => void + defineCustomElement?: () => void, + hasDelegateHost?: boolean ) => { if (defineCustomElement) { defineCustomElement(); @@ -116,6 +117,18 @@ export const createInlineOverlayComponent = ( style, }; + /** + * Some overlays need `.ion-page` so content + * takes up the full size of the parent overlay. + */ + const getWrapperClasses = () => { + if (hasDelegateHost) { + return `${DELEGATE_HOST} ion-page`; + } + + return DELEGATE_HOST; + }; + return createElement( 'template', {}, @@ -132,14 +145,8 @@ export const createInlineOverlayComponent = ( ? createElement( 'div', { - id: 'ion-react-wrapper', ref: this.wrapperRef, - className: 'ion-delegate-host', - style: { - display: 'flex', - flexDirection: 'column', - height: '100%', - }, + className: getWrapperClasses(), }, children ) @@ -194,3 +201,5 @@ export const createInlineOverlayComponent = ( }; return createForwardRef(ReactComponent, displayName); }; + +const DELEGATE_HOST = 'ion-delegate-host'; diff --git a/packages/react/test/base/src/App.tsx b/packages/react/test/base/src/App.tsx index 2003e4295b..9278550f15 100644 --- a/packages/react/test/base/src/App.tsx +++ b/packages/react/test/base/src/App.tsx @@ -32,6 +32,7 @@ import IonModalConditionalSibling from './pages/overlay-components/IonModalCondi import IonModalConditional from './pages/overlay-components/IonModalConditional'; import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton'; import IonPopoverNested from './pages/overlay-components/IonPopoverNested'; +import IonModalMultipleChildren from './pages/overlay-components/IonModalMultipleChildren'; setupIonicReact(); @@ -52,6 +53,10 @@ const App: React.FC = () => ( path="/overlay-components/modal-datetime-button" component={IonModalDatetimeButton} /> + diff --git a/packages/react/test/base/src/pages/overlay-components/IonModalMultipleChildren.tsx b/packages/react/test/base/src/pages/overlay-components/IonModalMultipleChildren.tsx new file mode 100644 index 0000000000..9919ab8d44 --- /dev/null +++ b/packages/react/test/base/src/pages/overlay-components/IonModalMultipleChildren.tsx @@ -0,0 +1,18 @@ +import { IonButton, IonContent, IonModal } from '@ionic/react'; + +/** + * Test inline modal rendering when content lacks a single root node + */ +const IonModalMultipleChildren = () => { + return ( + + Show Modal + +
Content A
+
Content B
+
+
+ ); +}; + +export default IonModalMultipleChildren; diff --git a/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts index cfc9d1ad3d..b3dc847074 100644 --- a/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts +++ b/packages/react/test/base/tests/e2e/specs/overlay-components/IonModal.cy.ts @@ -76,3 +76,16 @@ describe('IonModal: conditional rendering', () => { }); }); + +describe('IonModal: multiple children', () => { + it('should render a root .ion-page when passed multiple children', () => { + cy.visit('/overlay-components/modal-multiple-children'); + + cy.get('ion-button#show-modal').click(); + + cy.get('ion-modal').should('be.visible'); + + cy.get('ion-modal .ion-page').should('have.length', 1); + cy.get('ion-modal .ion-page .child-content').should('have.length', 2); + }); +}); diff --git a/packages/vue/scripts/copy-overlays.js b/packages/vue/scripts/copy-overlays.js index 42bb2225bb..aece571cac 100644 --- a/packages/vue/scripts/copy-overlays.js +++ b/packages/vue/scripts/copy-overlays.js @@ -25,7 +25,8 @@ function generateOverlays() { }, { tag: 'ion-modal', - name: 'IonModal' + name: 'IonModal', + hasDelegateHost: true }, { tag: 'ion-popover', @@ -44,8 +45,10 @@ function generateOverlays() { componentImports.push(`import { defineCustomElement as ${defineCustomElementFn} } from '@ionic/core/components/${component.tag}.js'`); + const delegateHostString = component.hasDelegateHost ? ', true' : ''; + componentDefinitions.push(` -export const ${component.name} = /*@__PURE__*/ defineOverlayContainer('${component.tag}', ${defineCustomElementFn}, [${props.join(', ')}]); +export const ${component.name} = /*@__PURE__*/ defineOverlayContainer('${component.tag}', ${defineCustomElementFn}, [${props.join(', ')}]${delegateHostString}); `); }); diff --git a/packages/vue/src/components/Overlays.ts b/packages/vue/src/components/Overlays.ts index 16deb3a563..62df98436b 100644 --- a/packages/vue/src/components/Overlays.ts +++ b/packages/vue/src/components/Overlays.ts @@ -27,7 +27,7 @@ export const IonPicker = /*@__PURE__*/ defineOverlayContainer('io export const IonToast = /*@__PURE__*/ defineOverlayContainer('ion-toast', defineIonToastCustomElement, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'htmlAttributes', 'icon', 'isOpen', 'keyboardClose', 'layout', 'leaveAnimation', 'message', 'mode', 'position', 'translucent', 'trigger']); -export const IonModal = /*@__PURE__*/ defineOverlayContainer('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'trigger']); +export const IonModal = /*@__PURE__*/ defineOverlayContainer('ion-modal', defineIonModalCustomElement, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'canDismiss', 'enterAnimation', 'handle', 'handleBehavior', 'htmlAttributes', 'initialBreakpoint', 'isOpen', 'keepContentsMounted', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'trigger'], true); export const IonPopover = /*@__PURE__*/ defineOverlayContainer('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']); diff --git a/packages/vue/src/vue-component-lib/overlays.ts b/packages/vue/src/vue-component-lib/overlays.ts index d47cceefa1..451dfafbfa 100644 --- a/packages/vue/src/vue-component-lib/overlays.ts +++ b/packages/vue/src/vue-component-lib/overlays.ts @@ -9,7 +9,7 @@ export interface OverlayProps { const EMPTY_PROP = Symbol(); const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP }; -export const defineOverlayContainer = (name: string, defineCustomElement: () => void, componentProps: string[] = [], controller?: any) => { +export const defineOverlayContainer = (name: string, defineCustomElement: () => void, componentProps: string[] = [], hasDelegateHost?: boolean, controller?: any) => { const createControllerComponent = () => { return defineComponent((props, { slots, emit }) => { @@ -162,10 +162,22 @@ export const defineOverlayContainer = (name: string, defin } } + /** + * Some overlays need a wrapper element so content + * takes up the full size of the parent overlay. + */ + const renderChildren = () => { + if (hasDelegateHost) { + return h('div', { className: 'ion-delegate-host ion-page' }, slots); + } + + return slots; + } + return h( name, { ...restOfProps, ref: elementRef }, - (isOpen.value || restOfProps.keepContentsMounted) ? slots : undefined + (isOpen.value || restOfProps.keepContentsMounted) ? renderChildren() : undefined ) } }); diff --git a/packages/vue/test/base/src/router/index.ts b/packages/vue/test/base/src/router/index.ts index 236ec08afa..a62a9ceefb 100644 --- a/packages/vue/test/base/src/router/index.ts +++ b/packages/vue/test/base/src/router/index.ts @@ -29,6 +29,10 @@ const routes: Array = [ path: '/overlays', component: () => import('@/views/Overlays.vue') }, + { + path: '/modal-multiple-children', + component: () => import('@/views/ModalMultipleChildren.vue') + }, { path: '/keep-contents-mounted', component: () => import('@/views/OverlaysKeepContentsMounted.vue') diff --git a/packages/vue/test/base/src/views/ModalMultipleChildren.vue b/packages/vue/test/base/src/views/ModalMultipleChildren.vue new file mode 100644 index 0000000000..ec5784e53f --- /dev/null +++ b/packages/vue/test/base/src/views/ModalMultipleChildren.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/vue/test/base/tests/e2e/specs/modal-multiple-children.cy.js b/packages/vue/test/base/tests/e2e/specs/modal-multiple-children.cy.js new file mode 100644 index 0000000000..c4fec5ebc1 --- /dev/null +++ b/packages/vue/test/base/tests/e2e/specs/modal-multiple-children.cy.js @@ -0,0 +1,12 @@ +describe('modal - multiple children', () => { + it('should render a root .ion-page when passed multiple children', () => { + cy.visit('/modal-multiple-children'); + + cy.get('ion-button#show-modal').click(); + + cy.get('ion-modal').should('be.visible'); + + cy.get('ion-modal .ion-page').should('have.length', 1); + cy.get('ion-modal .ion-page .child-content').should('have.length', 2); + }); +})