mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 01:03:03 +08:00
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:eb2772c0ce/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`:eb2772c0ce/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
This commit is contained in:
@ -5,5 +5,6 @@ import { createInlineOverlayComponent } from './createInlineOverlayComponent';
|
|||||||
|
|
||||||
export const IonModal = /*@__PURE__*/ createInlineOverlayComponent<JSX.IonModal, HTMLIonModalElement>(
|
export const IonModal = /*@__PURE__*/ createInlineOverlayComponent<JSX.IonModal, HTMLIonModalElement>(
|
||||||
'ion-modal',
|
'ion-modal',
|
||||||
defineCustomElement
|
defineCustomElement,
|
||||||
|
true
|
||||||
);
|
);
|
||||||
|
@ -29,7 +29,8 @@ interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<Elem
|
|||||||
|
|
||||||
export const createInlineOverlayComponent = <PropType, ElementType>(
|
export const createInlineOverlayComponent = <PropType, ElementType>(
|
||||||
tagName: string,
|
tagName: string,
|
||||||
defineCustomElement?: () => void
|
defineCustomElement?: () => void,
|
||||||
|
hasDelegateHost?: boolean
|
||||||
) => {
|
) => {
|
||||||
if (defineCustomElement) {
|
if (defineCustomElement) {
|
||||||
defineCustomElement();
|
defineCustomElement();
|
||||||
@ -116,6 +117,18 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
|
|||||||
style,
|
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(
|
return createElement(
|
||||||
'template',
|
'template',
|
||||||
{},
|
{},
|
||||||
@ -132,14 +145,8 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
|
|||||||
? createElement(
|
? createElement(
|
||||||
'div',
|
'div',
|
||||||
{
|
{
|
||||||
id: 'ion-react-wrapper',
|
|
||||||
ref: this.wrapperRef,
|
ref: this.wrapperRef,
|
||||||
className: 'ion-delegate-host',
|
className: getWrapperClasses(),
|
||||||
style: {
|
|
||||||
display: 'flex',
|
|
||||||
flexDirection: 'column',
|
|
||||||
height: '100%',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
children
|
children
|
||||||
)
|
)
|
||||||
@ -194,3 +201,5 @@ export const createInlineOverlayComponent = <PropType, ElementType>(
|
|||||||
};
|
};
|
||||||
return createForwardRef<PropType, ElementType>(ReactComponent, displayName);
|
return createForwardRef<PropType, ElementType>(ReactComponent, displayName);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DELEGATE_HOST = 'ion-delegate-host';
|
||||||
|
@ -32,6 +32,7 @@ import IonModalConditionalSibling from './pages/overlay-components/IonModalCondi
|
|||||||
import IonModalConditional from './pages/overlay-components/IonModalConditional';
|
import IonModalConditional from './pages/overlay-components/IonModalConditional';
|
||||||
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
|
import IonModalDatetimeButton from './pages/overlay-components/IonModalDatetimeButton';
|
||||||
import IonPopoverNested from './pages/overlay-components/IonPopoverNested';
|
import IonPopoverNested from './pages/overlay-components/IonPopoverNested';
|
||||||
|
import IonModalMultipleChildren from './pages/overlay-components/IonModalMultipleChildren';
|
||||||
|
|
||||||
setupIonicReact();
|
setupIonicReact();
|
||||||
|
|
||||||
@ -52,6 +53,10 @@ const App: React.FC = () => (
|
|||||||
path="/overlay-components/modal-datetime-button"
|
path="/overlay-components/modal-datetime-button"
|
||||||
component={IonModalDatetimeButton}
|
component={IonModalDatetimeButton}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/overlay-components/modal-multiple-children"
|
||||||
|
component={IonModalMultipleChildren}
|
||||||
|
/>
|
||||||
<Route path="/keep-contents-mounted" component={KeepContentsMounted} />
|
<Route path="/keep-contents-mounted" component={KeepContentsMounted} />
|
||||||
<Route path="/navigation" component={NavComponent} />
|
<Route path="/navigation" component={NavComponent} />
|
||||||
<Route path="/tabs" component={Tabs} />
|
<Route path="/tabs" component={Tabs} />
|
||||||
|
@ -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 (
|
||||||
|
<IonContent>
|
||||||
|
<IonButton id="show-modal">Show Modal</IonButton>
|
||||||
|
<IonModal trigger="show-modal">
|
||||||
|
<div className="child-content">Content A</div>
|
||||||
|
<div className="child-content">Content B</div>
|
||||||
|
</IonModal>
|
||||||
|
</IonContent>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default IonModalMultipleChildren;
|
@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -25,7 +25,8 @@ function generateOverlays() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'ion-modal',
|
tag: 'ion-modal',
|
||||||
name: 'IonModal'
|
name: 'IonModal',
|
||||||
|
hasDelegateHost: true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
tag: 'ion-popover',
|
tag: 'ion-popover',
|
||||||
@ -44,8 +45,10 @@ function generateOverlays() {
|
|||||||
|
|
||||||
componentImports.push(`import { defineCustomElement as ${defineCustomElementFn} } from '@ionic/core/components/${component.tag}.js'`);
|
componentImports.push(`import { defineCustomElement as ${defineCustomElementFn} } from '@ionic/core/components/${component.tag}.js'`);
|
||||||
|
|
||||||
|
const delegateHostString = component.hasDelegateHost ? ', true' : '';
|
||||||
|
|
||||||
componentDefinitions.push(`
|
componentDefinitions.push(`
|
||||||
export const ${component.name} = /*@__PURE__*/ defineOverlayContainer<JSX.${component.name}>('${component.tag}', ${defineCustomElementFn}, [${props.join(', ')}]);
|
export const ${component.name} = /*@__PURE__*/ defineOverlayContainer<JSX.${component.name}>('${component.tag}', ${defineCustomElementFn}, [${props.join(', ')}]${delegateHostString});
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ export const IonPicker = /*@__PURE__*/ defineOverlayContainer<JSX.IonPicker>('io
|
|||||||
|
|
||||||
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']);
|
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']);
|
||||||
|
|
||||||
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']);
|
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);
|
||||||
|
|
||||||
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']);
|
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']);
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ export interface OverlayProps {
|
|||||||
const EMPTY_PROP = Symbol();
|
const EMPTY_PROP = Symbol();
|
||||||
const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP };
|
const DEFAULT_EMPTY_PROP = { default: EMPTY_PROP };
|
||||||
|
|
||||||
export const defineOverlayContainer = <Props extends object>(name: string, defineCustomElement: () => void, componentProps: string[] = [], controller?: any) => {
|
export const defineOverlayContainer = <Props extends object>(name: string, defineCustomElement: () => void, componentProps: string[] = [], hasDelegateHost?: boolean, controller?: any) => {
|
||||||
|
|
||||||
const createControllerComponent = () => {
|
const createControllerComponent = () => {
|
||||||
return defineComponent<Props & OverlayProps>((props, { slots, emit }) => {
|
return defineComponent<Props & OverlayProps>((props, { slots, emit }) => {
|
||||||
@ -162,10 +162,22 @@ export const defineOverlayContainer = <Props extends object>(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(
|
return h(
|
||||||
name,
|
name,
|
||||||
{ ...restOfProps, ref: elementRef },
|
{ ...restOfProps, ref: elementRef },
|
||||||
(isOpen.value || restOfProps.keepContentsMounted) ? slots : undefined
|
(isOpen.value || restOfProps.keepContentsMounted) ? renderChildren() : undefined
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -29,6 +29,10 @@ const routes: Array<RouteRecordRaw> = [
|
|||||||
path: '/overlays',
|
path: '/overlays',
|
||||||
component: () => import('@/views/Overlays.vue')
|
component: () => import('@/views/Overlays.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/modal-multiple-children',
|
||||||
|
component: () => import('@/views/ModalMultipleChildren.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/keep-contents-mounted',
|
path: '/keep-contents-mounted',
|
||||||
component: () => import('@/views/OverlaysKeepContentsMounted.vue')
|
component: () => import('@/views/OverlaysKeepContentsMounted.vue')
|
||||||
|
16
packages/vue/test/base/src/views/ModalMultipleChildren.vue
Normal file
16
packages/vue/test/base/src/views/ModalMultipleChildren.vue
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<template>
|
||||||
|
<ion-page data-pageid="modal-multiple-children">
|
||||||
|
<ion-content class="ion-padding" :fullscreen="true">
|
||||||
|
<ion-button id="show-modal">Show Modal</ion-button>
|
||||||
|
|
||||||
|
<ion-modal trigger="show-modal">
|
||||||
|
<div class="child-content">Content A</div>
|
||||||
|
<div class="child-content">Content B</div>
|
||||||
|
</ion-modal>
|
||||||
|
</ion-content>
|
||||||
|
</ion-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { IonButton, IonContent, IonPage, IonModal } from '@ionic/vue';
|
||||||
|
</script>
|
@ -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);
|
||||||
|
});
|
||||||
|
})
|
Reference in New Issue
Block a user