Compare commits

..

24 Commits

Author SHA1 Message Date
Liam DeBeasi
51dae5a4d6 6.0.0-beta.5 2021-09-01 11:27:29 -04:00
Liam DeBeasi
00bae431d4 chore(): build vue with bottom sheet changes 2021-09-01 11:15:40 -04:00
Liam DeBeasi
7a50992cb0 chore(): sync with main
chore(): sync with main for v6 beta 5 release
2021-09-01 11:03:52 -04:00
Liam DeBeasi
c75951354b chore(): remove duplicate export 2021-09-01 10:33:27 -04:00
Liam DeBeasi
b211cf0236 chore(): sync with main for beta 5 release 2021-09-01 10:14:58 -04:00
Liam DeBeasi
e512fc1ecd merge release-5.7.0
5.7.0
2021-09-01 10:07:36 -04:00
Liam DeBeasi
a12d146c3e 5.7.0 2021-09-01 09:43:23 -04:00
Liam DeBeasi
a0229bc7b2 refactor(virtual-scroll): deprecate virtual scroll in favor of JS framework solutions (#23854) 2021-08-31 18:20:29 -04:00
William Martin
11fda41420 feat(slides): add IonicSlides module for Swiper migration, deprecate ion-slides (#23844)
backports #23447
2021-08-31 17:52:47 -04:00
William Martin
1680b0ce9f refactor(react): transition to Stencil React bindings (#23826) 2021-08-31 17:29:59 -04:00
Liam DeBeasi
5ca2ce9197 fix(item): form validation states are now properly shown (#23853)
resolves #23733 #23850

Co-authored-by: Will Martin <willmartindev@users.noreply.github.com>
2021-08-31 17:18:21 -04:00
Liam DeBeasi
12216d378d feat(modal): add bottom sheet functionality (#23828)
resolves #21039
2021-08-31 15:19:19 -04:00
Liam DeBeasi
c925274c3b fix(angular): overlay interfaces are now properly exported (#23847)
resolves #23846
2021-08-30 10:35:03 -04:00
Ikko Ashimine
33f0bcd437 chore(slides): fix typo in image slides example (#23838) 2021-08-30 10:15:12 -04:00
Liam DeBeasi
a212eb5259 fix(overlays): thrown errors are no longer suppressed (#23831)
resolves #22724
2021-08-27 12:51:17 -04:00
Liam DeBeasi
1d2ee92ca0 feat(popover): add ability to pass event to present method (#23827)
resolves #23813
2021-08-26 16:22:04 -04:00
Liam DeBeasi
3c442228ff fix(vue): router guards are now fired correctly when written in a component (#23821)
resolves #23820
2021-08-26 10:40:48 -04:00
William Martin
950350a948 fix(datetime): prevent vertical page scroll on interaction (#23780)
resolves #23554
2021-08-26 09:18:51 -04:00
Liam DeBeasi
50b88b24c2 chore(): run build to update picker readme (#23822) 2021-08-25 18:57:26 -04:00
Sarah Drasner
9317f6eb41 docs(picker): add a simple picker example for Vue (#23818)
There was previously no example for a vue picker, this adds one.

closes #2053
2021-08-25 17:50:33 -04:00
Liam DeBeasi
9932e26a2e fix(label): label now only takes up as much space as needed when slotted (#23807)
resolves #23806
2021-08-24 10:25:12 -04:00
Toby Smith
02409f2abf fix(reorder-group): dragging reorder item to bottom no longer gives out of bounds index (#23797)
resolves #23796
2021-08-23 09:38:27 -04:00
Liam DeBeasi
864212b0f2 fix(alert): AlertButton role now has correct types (#23791) 2021-08-18 13:43:12 -04:00
Liam DeBeasi
3d1ae0305d merge release-6.0.0-beta.4
6.0.0-beta.4
2021-08-18 11:51:10 -04:00
90 changed files with 2631 additions and 588 deletions

View File

@@ -1,4 +1,43 @@
# [6.0.0-beta.4](https://github.com/ionic-team/ionic/compare/v5.6.14...v6.0.0-beta.4) (2021-08-18)
# [6.0.0-beta.5](https://github.com/ionic-team/ionic/compare/v6.0.0-beta.4...v6.0.0-beta.5) (2021-09-01)
### Bug Fixes
* **angular:** overlay interfaces are now properly exported ([#23847](https://github.com/ionic-team/ionic/issues/23847)) ([c925274](https://github.com/ionic-team/ionic/commit/c925274c3bb22532a323b2a07771d7448f7de542)), closes [#23846](https://github.com/ionic-team/ionic/issues/23846)
* **datetime:** prevent vertical page scroll on interaction ([#23780](https://github.com/ionic-team/ionic/issues/23780)) ([950350a](https://github.com/ionic-team/ionic/commit/950350a948320f889589a0c9d2ec9045637215e5)), closes [#23554](https://github.com/ionic-team/ionic/issues/23554)
* **item:** form validation states are now properly shown ([#23853](https://github.com/ionic-team/ionic/issues/23853)) ([5ca2ce9](https://github.com/ionic-team/ionic/commit/5ca2ce91971408218d7bdc52509ce61a6ebb46aa)), closes [#23733](https://github.com/ionic-team/ionic/issues/23733) [#23850](https://github.com/ionic-team/ionic/issues/23850)
* **overlays:** thrown errors are no longer suppressed ([#23831](https://github.com/ionic-team/ionic/issues/23831)) ([a212eb5](https://github.com/ionic-team/ionic/commit/a212eb52599e35d3706e2d3cef751e490e3a7259)), closes [#22724](https://github.com/ionic-team/ionic/issues/22724)
### Features
* **modal:** add bottom sheet functionality ([#23828](https://github.com/ionic-team/ionic/issues/23828)) ([12216d3](https://github.com/ionic-team/ionic/commit/12216d378df091e16fd77d271b107e819278481c)), closes [#21039](https://github.com/ionic-team/ionic/issues/21039)
* **popover:** add ability to pass event to present method ([#23827](https://github.com/ionic-team/ionic/issues/23827)) ([1d2ee92](https://github.com/ionic-team/ionic/commit/1d2ee92ca01b77bcf87c7783b50d59efcf0a402a)), closes [#23813](https://github.com/ionic-team/ionic/issues/23813)
# [5.7.0 Potassium](https://github.com/ionic-team/ionic/compare/v5.6.14...v5.7.0) (2021-09-01)
### Bug Fixes
* **alert:** AlertButton role now has correct types ([#23791](https://github.com/ionic-team/ionic/issues/23791)) ([864212b](https://github.com/ionic-team/ionic/commit/864212b0f28d33daede5f4767aa03efa37c219ae))
* **label:** label now only takes up as much space as needed when slotted ([#23807](https://github.com/ionic-team/ionic/issues/23807)) ([9932e26](https://github.com/ionic-team/ionic/commit/9932e26a2ef28317bc85761e71a8fc4d881b8ae8)), closes [#23806](https://github.com/ionic-team/ionic/issues/23806)
* **reorder-group:** dragging reorder item to bottom no longer gives out of bounds index ([#23797](https://github.com/ionic-team/ionic/issues/23797)) ([02409f2](https://github.com/ionic-team/ionic/commit/02409f2abfa8acbab05d0f1217b9d1c13721746e)), closes [#23796](https://github.com/ionic-team/ionic/issues/23796)
* **vue:** router guards are now fire correctly when written in a component ([#23821](https://github.com/ionic-team/ionic/issues/23821)) ([3c44222](https://github.com/ionic-team/ionic/commit/3c442228ff746165fd823687a2661a24edd08820)), closes [#23820](https://github.com/ionic-team/ionic/issues/23820)
### Features
* **slides:** add IonicSlides module for Swiper migration, deprecate ion-slides ([#23844](https://github.com/ionic-team/ionic/issues/23844)) ([11fda41](https://github.com/ionic-team/ionic/commit/11fda41420343886dabd97096690be38f1c40524)), closes [#23447](https://github.com/ionic-team/ionic/issues/23447)
### Code Refactoring
* **virtual-scroll:** deprecated virtual scroll in favor of solutions provided by JS frameworks ([#23854](https://github.com/ionic-team/ionic-framework/pull/23854)) ([a0229bc](https://github.com/ionic-team/ionic-framework/commit/a0229bc7b2edb061510de0f2042e7910d04accc0))
# [6.0.0-beta.4](https://github.com/ionic-team/ionic/compare/v6.0.0-beta.3...v6.0.0-beta.4) (2021-08-18)
### Bug Fixes

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/angular",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"license": "MIT",
"dependencies": {
"@ionic/core": "6.0.0-beta.3",
"@ionic/core": "6.0.0-beta.4",
"tslib": "^1.9.3"
},
"devDependencies": {
@@ -204,9 +204,9 @@
}
},
"node_modules/@ionic/core": {
"version": "6.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.3.tgz",
"integrity": "sha512-lkSjMPdNwkqJ2rJfyTEy8W9WyTTq+rpvci5ZBKXhqNCBdIhXRxBKNPDHjI8B2qbleEKAu05hJMUFMZTGFSDN8w==",
"version": "6.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.4.tgz",
"integrity": "sha512-yjw0v/NTdxUiBwyWydwOliFHHxE8t5iQy3Sl3TVLlKV9Dx6xuSRHJAiFf+p57KUeST+M8EvDwdgIffLAT9U93g==",
"dependencies": {
"@stencil/core": "^2.6.0",
"ionicons": "^5.5.1",
@@ -5156,9 +5156,9 @@
}
},
"@ionic/core": {
"version": "6.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.3.tgz",
"integrity": "sha512-lkSjMPdNwkqJ2rJfyTEy8W9WyTTq+rpvci5ZBKXhqNCBdIhXRxBKNPDHjI8B2qbleEKAu05hJMUFMZTGFSDN8w==",
"version": "6.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.4.tgz",
"integrity": "sha512-yjw0v/NTdxUiBwyWydwOliFHHxE8t5iQy3Sl3TVLlKV9Dx6xuSRHJAiFf+p57KUeST+M8EvDwdgIffLAT9U93g==",
"requires": {
"@stencil/core": "^2.6.0",
"ionicons": "^5.5.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "Angular specific wrappers for @ionic/core",
"keywords": [
"ionic",
@@ -42,7 +42,7 @@
"validate": "npm i && npm run lint && npm run test && npm run build"
},
"dependencies": {
"@ionic/core": "6.0.0-beta.4",
"@ionic/core": "6.0.0-beta.5",
"tslib": "^1.9.3"
},
"peerDependencies": {

View File

@@ -14,6 +14,8 @@ export { IonVirtualScroll } from './directives/virtual-scroll/virtual-scroll';
export { VirtualItem } from './directives/virtual-scroll/virtual-item';
export { VirtualHeader } from './directives/virtual-scroll/virtual-header';
export { VirtualFooter } from './directives/virtual-scroll/virtual-footer';
export { IonModal } from './directives/overlays/modal';
export { IonPopover } from './directives/overlays/popover';
export * from './directives/proxies';
// PROVIDERS

View File

@@ -13,8 +13,8 @@ import { IonRouterOutlet } from './directives/navigation/ion-router-outlet';
import { IonTabs } from './directives/navigation/ion-tabs';
import { NavDelegate } from './directives/navigation/nav-delegate';
import { RouterLinkDelegate } from './directives/navigation/router-link-delegate';
import { IonModal } from './directives/overlays/ion-modal';
import { IonPopover } from './directives/overlays/ion-popover';
import { IonModal } from './directives/overlays/modal';
import { IonPopover } from './directives/overlays/popover';
import { IonAccordion, IonAccordionGroup, IonApp, IonAvatar, IonBackButton, IonBackdrop, IonBadge, IonBreadcrumb, IonBreadcrumbs, IonButton, IonButtons, IonCard, IonCardContent, IonCardHeader, IonCardSubtitle, IonCardTitle, IonCheckbox, IonChip, IonCol, IonContent, IonDatetime, IonFab, IonFabButton, IonFabList, IonFooter, IonGrid, IonHeader, IonIcon, IonImg, IonInfiniteScroll, IonInfiniteScrollContent, IonInput, IonItem, IonItemDivider, IonItemGroup, IonItemOption, IonItemOptions, IonItemSliding, IonLabel, IonList, IonListHeader, IonMenu, IonMenuButton, IonMenuToggle, IonNav, IonNavLink, IonNote, IonProgressBar, IonRadio, IonRadioGroup, IonRange, IonRefresher, IonRefresherContent, IonReorder, IonReorderGroup, IonRippleEffect, IonRow, IonSearchbar, IonSegment, IonSegmentButton, IonSelect, IonSelectOption, IonSkeletonText, IonSlide, IonSlides, IonSpinner, IonSplitPane, IonTabBar, IonTabButton, IonText, IonTextarea, IonThumbnail, IonTitle, IonToggle, IonToolbar } from './directives/proxies';
import { VirtualFooter } from './directives/virtual-scroll/virtual-footer';
import { VirtualHeader } from './directives/virtual-scroll/virtual-header';

View File

@@ -758,8 +758,12 @@ ion-menu-toggle,prop,menu,string | undefined,undefined,false,false
ion-modal,shadow
ion-modal,prop,animated,boolean,true,false,false
ion-modal,prop,backdropBreakpoint,number,0,false,false
ion-modal,prop,backdropDismiss,boolean,true,false,false
ion-modal,prop,breakpoints,number[] | undefined,undefined,false,false
ion-modal,prop,enterAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
ion-modal,prop,handle,boolean | undefined,undefined,false,false
ion-modal,prop,initialBreakpoint,number | undefined,undefined,false,false
ion-modal,prop,isOpen,boolean,false,false,false
ion-modal,prop,keyboardClose,boolean,true,false,false
ion-modal,prop,leaveAnimation,((baseEl: any, opts?: any) => Animation) | undefined,undefined,false,false
@@ -794,6 +798,7 @@ ion-modal,css-prop,--min-width
ion-modal,css-prop,--width
ion-modal,part,backdrop
ion-modal,part,content
ion-modal,part,handle
ion-nav,shadow
ion-nav,prop,animated,boolean,true,false,false
@@ -887,7 +892,7 @@ ion-popover,prop,triggerAction,"click" | "context-menu" | "hover",'click',false,
ion-popover,method,dismiss,dismiss(data?: any, role?: string | undefined, dismissParentPopover?: boolean) => Promise<boolean>
ion-popover,method,onDidDismiss,onDidDismiss<T = any>() => Promise<OverlayEventDetail<T>>
ion-popover,method,onWillDismiss,onWillDismiss<T = any>() => Promise<OverlayEventDetail<T>>
ion-popover,method,present,present() => Promise<void>
ion-popover,method,present,present(event?: MouseEvent | TouchEvent | PointerEvent | undefined) => Promise<void>
ion-popover,event,didDismiss,OverlayEventDetail<any>,true
ion-popover,event,didPresent,void,true
ion-popover,event,ionPopoverDidDismiss,OverlayEventDetail<any>,true

20
core/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/core",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/core",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"license": "MIT",
"dependencies": {
"@stencil/core": "^2.6.0",
@@ -18,6 +18,7 @@
"@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/react-output-target": "^0.0.12",
"@stencil/sass": "1.3.2",
"@stencil/vue-output-target": "^0.5.1",
"@types/jest": "^26.0.20",
@@ -1367,6 +1368,15 @@
"npm": ">=6.0.0"
}
},
"node_modules/@stencil/react-output-target": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@stencil/react-output-target/-/react-output-target-0.0.12.tgz",
"integrity": "sha512-X/lWAI/FW4tg/pjwe5UWy8KbRk2vWcWR+S6tBqNzKO6pKD6qr60dfajN13EO9nnm5hGr48FP1m/M8kqFbjpZrg==",
"dev": true,
"peerDependencies": {
"@stencil/core": ">=1.8.0"
}
},
"node_modules/@stencil/sass": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-1.3.2.tgz",
@@ -15010,6 +15020,12 @@
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.6.0.tgz",
"integrity": "sha512-QsxWayZyusnqSZrlCl81R71rA3KqFjVVQSH4E0rGN15F1GdQaFonKlHLyCOLKLig1zzC+DQkLLiUuocexuvdeQ=="
},
"@stencil/react-output-target": {
"version": "0.0.12",
"resolved": "https://registry.npmjs.org/@stencil/react-output-target/-/react-output-target-0.0.12.tgz",
"integrity": "sha512-X/lWAI/FW4tg/pjwe5UWy8KbRk2vWcWR+S6tBqNzKO6pKD6qr60dfajN13EO9nnm5hGr48FP1m/M8kqFbjpZrg==",
"dev": true
},
"@stencil/sass": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@stencil/sass/-/sass-1.3.2.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/core",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "Base components for Ionic",
"keywords": [
"ionic",
@@ -40,6 +40,7 @@
"@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/react-output-target": "^0.0.12",
"@stencil/sass": "1.3.2",
"@stencil/vue-output-target": "^0.5.1",
"@types/jest": "^26.0.20",

View File

@@ -1465,10 +1465,18 @@ export namespace Components {
* If `true`, the modal will animate.
*/
"animated": boolean;
/**
* A decimal value between 0 and 1 that indicates the point at which the backdrop will begin to fade in when using a sheet modal. Prior to this point, the backdrop will be hidden and the content underneath the sheet can be interacted with. This value must also be listed in the `breakpoints` array.
*/
"backdropBreakpoint": number;
/**
* If `true`, the modal will be dismissed when the backdrop is clicked.
*/
"backdropDismiss": boolean;
/**
* The breakpoints to use when creating a sheet modal. Each value in the array must be a decimal between 0 and 1 where 0 indicates the modal is fully closed and 1 indicates the modal is fully open. Values are relative to the height of the modal, not the height of the screen. One of the values in this array must be the value of the `initialBreakpoint` property. For example: [0, .25, .5, 1]
*/
"breakpoints"?: number[];
/**
* The component to display inside of the modal.
*/
@@ -1492,6 +1500,14 @@ export namespace Components {
* Animation to use when the modal is presented.
*/
"enterAnimation"?: AnimationBuilder;
/**
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
*/
"handle"?: boolean;
/**
* A decimal value between 0 and 1 that indicates the initial point the modal will open at when creating a sheet modal. This value must also be listed in the `breakpoints` array.
*/
"initialBreakpoint"?: number;
/**
* If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code.
*/
@@ -1829,9 +1845,9 @@ export namespace Components {
"onWillDismiss": <T = any>() => Promise<OverlayEventDetail<T>>;
"overlayIndex": number;
/**
* Present the popover overlay after it has been created.
* Present the popover overlay after it has been created. Developers can pass a mouse, touch, or pointer event to position the popover relative to where that event was dispatched.
*/
"present": () => Promise<void>;
"present": (event?: MouseEvent | TouchEvent | PointerEvent | undefined) => Promise<void>;
/**
* When opening a popover from a trigger, we should not be modifying the `event` prop from inside the component. Additionally, when pressing the "Right" arrow key, we need to shift focus to the first descendant in the newly presented popover.
*/
@@ -5061,10 +5077,18 @@ declare namespace LocalJSX {
* If `true`, the modal will animate.
*/
"animated"?: boolean;
/**
* A decimal value between 0 and 1 that indicates the point at which the backdrop will begin to fade in when using a sheet modal. Prior to this point, the backdrop will be hidden and the content underneath the sheet can be interacted with. This value must also be listed in the `breakpoints` array.
*/
"backdropBreakpoint"?: number;
/**
* If `true`, the modal will be dismissed when the backdrop is clicked.
*/
"backdropDismiss"?: boolean;
/**
* The breakpoints to use when creating a sheet modal. Each value in the array must be a decimal between 0 and 1 where 0 indicates the modal is fully closed and 1 indicates the modal is fully open. Values are relative to the height of the modal, not the height of the screen. One of the values in this array must be the value of the `initialBreakpoint` property. For example: [0, .25, .5, 1]
*/
"breakpoints"?: number[];
/**
* The component to display inside of the modal.
*/
@@ -5082,6 +5106,14 @@ declare namespace LocalJSX {
* Animation to use when the modal is presented.
*/
"enterAnimation"?: AnimationBuilder;
/**
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties.
*/
"handle"?: boolean;
/**
* A decimal value between 0 and 1 that indicates the initial point the modal will open at when creating a sheet modal. This value must also be listed in the `breakpoints` array.
*/
"initialBreakpoint"?: number;
/**
* If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code.
*/

View File

@@ -44,7 +44,7 @@ export interface AlertInputAttributes extends JSXBase.InputHTMLAttributes<HTMLIn
export interface AlertButton {
text: string;
role?: string;
role?: 'cancel' | 'destructive' | string;
cssClass?: string | string[];
id?: string;
handler?: (value: any) => boolean | void | {[key: string]: any};

View File

@@ -48,7 +48,7 @@ Any of the defined [CSS Custom Properties](#css-custom-properties) can be used t
```typescript
interface AlertButton {
text: string;
role?: string;
role?: 'cancel' | 'destructive' | string;
cssClass?: string | string[];
handler?: (value: any) => boolean | void | {[key: string]: any};
}

View File

@@ -725,7 +725,7 @@ export class Datetime implements ComponentInterface {
year
});
workingMonth.scrollIntoView(false);
calendarBodyRef.scrollLeft = workingMonth.clientWidth;
calendarBodyRef.style.removeProperty('overflow');
calendarBodyRef.style.removeProperty('pointer-events');
@@ -967,11 +967,11 @@ export class Datetime implements ComponentInterface {
const yearEl = yearRef.querySelector(`.picker-col-item[data-value='${workingYear}']`);
if (monthEl) {
monthEl.scrollIntoView({ block: 'center', inline: 'center' });
this.centerPickerItemInView(monthEl as HTMLElement, monthRef, 'auto');
}
if (yearEl) {
yearEl.scrollIntoView({ block: 'center', inline: 'center' });
this.centerPickerItemInView(yearEl as HTMLElement, yearRef, 'auto');
}
});
}, 250);
@@ -1146,10 +1146,10 @@ export class Datetime implements ComponentInterface {
const nextMonth = calendarBodyRef.querySelector('.calendar-month:last-of-type');
if (!nextMonth) { return; }
nextMonth.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
calendarBodyRef.scrollTo({
top: 0,
left: (nextMonth as HTMLElement).offsetWidth * 2,
behavior: 'smooth'
});
}
@@ -1160,10 +1160,10 @@ export class Datetime implements ComponentInterface {
const prevMonth = calendarBodyRef.querySelector('.calendar-month:first-of-type');
if (!prevMonth) { return; }
prevMonth.scrollIntoView({
behavior: 'smooth',
block: 'end',
inline: 'nearest'
calendarBodyRef.scrollTo({
top: 0,
left: 0,
behavior: 'smooth'
});
}
@@ -1226,6 +1226,15 @@ export class Datetime implements ComponentInterface {
})
}
private centerPickerItemInView(target: HTMLElement, container: HTMLElement, behavior: ScrollBehavior = 'smooth') {
container.scroll({
// (Vertical offset from parent) - (three empty picker rows) + (half the height of the target to ensure the scroll triggers)
top: target.offsetTop - (3 * target.clientHeight) + (target.clientHeight / 2),
left: 0,
behavior
});
}
private renderiOSYearView(calendarYears: number[] = []) {
return [
<div class="datetime-picker-before"></div>,
@@ -1240,10 +1249,7 @@ export class Datetime implements ComponentInterface {
<div
class="picker-col-item"
data-value={month.value}
onClick={(ev: Event) => {
const target = ev.target as HTMLElement;
target.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' });
}}
onClick={(ev: Event) => this.centerPickerItemInView(ev.target as HTMLElement, this.monthRef as HTMLElement)}
>{month.text}</div>
)
})}
@@ -1260,10 +1266,7 @@ export class Datetime implements ComponentInterface {
<div
class="picker-col-item"
data-value={year}
onClick={(ev: Event) => {
const target = ev.target as HTMLElement;
target.scrollIntoView({ block: 'center', inline: 'center', behavior: 'smooth' });
}}
onClick={(ev: Event) => this.centerPickerItemInView(ev.target as HTMLElement, this.yearRef as HTMLElement)}
>{year}</div>
)
})}

View File

@@ -20,7 +20,7 @@
--padding-start: #{$item-md-padding-start};
--inner-padding-end: #{$item-md-padding-end};
--inner-border-width: #{0 0 $item-md-border-bottom-width 0};
--highlight-height: 2px;
--highlight-height: 1px;
--highlight-color-focused: #{$item-md-input-highlight-color};
--highlight-color-valid: #{$item-md-input-highlight-color-valid};
--highlight-color-invalid: #{$item-md-input-highlight-color-invalid};
@@ -31,6 +31,67 @@
text-transform: none;
}
:host(.item-fill-outline) {
--highlight-height: 2px;
}
// Item Fill: None
// --------------------------------------------------
:host(.item-fill-none.item-interactive.ion-focus) .item-highlight,
:host(.item-fill-none.item-interactive.item-has-focus) .item-highlight,
:host(.item-fill-none.item-interactive.ion-touched.ion-invalid) .item-highlight {
transform: scaleX(1);
border-width: 0 0 var(--full-highlight-height) 0;
border-style: var(--border-style);
border-color: var(--highlight-background);
}
:host(.item-fill-none.item-interactive.ion-focus) .item-native,
:host(.item-fill-none.item-interactive.item-has-focus) .item-native,
:host(.item-fill-none.item-interactive.ion-touched.ion-invalid) .item-native {
border-bottom-color: var(--highlight-background);
}
// Item Fill: Outline
// --------------------------------------------------
:host(.item-fill-outline.item-interactive.ion-focus) .item-highlight,
:host(.item-fill-outline.item-interactive.item-has-focus) .item-highlight {
transform: scaleX(1);
}
:host(.item-fill-outline.item-interactive.ion-focus) .item-highlight,
:host(.item-fill-outline.item-interactive.item-has-focus) .item-highlight,
:host(.item-fill-outline.item-interactive.ion-touched.ion-invalid) .item-highlight {
border-width: var(--full-highlight-height);
border-style: var(--border-style);
border-color: var(--highlight-background);
}
:host(.item-fill-outline.item-interactive.ion-touched.ion-invalid) .item-native {
border-color: var(--highlight-background);
}
// Item Fill: Solid
// --------------------------------------------------
:host(.item-fill-solid.item-interactive.ion-focus) .item-highlight,
:host(.item-fill-solid.item-interactive.item-has-focus) .item-highlight,
:host(.item-fill-solid.item-interactive.ion-touched.ion-invalid) .item-highlight {
transform: scaleX(1);
border-width: 0 0 var(--full-highlight-height) 0;
border-style: var(--border-style);
border-color: var(--highlight-background);
}
:host(.item-fill-solid.item-interactive.ion-focus) .item-native,
:host(.item-fill-solid.item-interactive.item-has-focus) .item-native,
:host(.item-fill-solid.item-interactive.ion-touched.ion-invalid) .item-native {
border-bottom-color: var(--highlight-background);
}
// Material Design Item: States
// --------------------------------------------------
@@ -329,7 +390,6 @@
:host(.item-fill-solid.ion-color),
:host(.item-fill-outline.ion-color) {
--highlight-background: #{current-color(base)};
--highlight-color-focused: #{current-color(base)};
}
@@ -353,8 +413,6 @@
:host(.item-fill-solid.ion-focused) .item-native,
:host(.item-fill-solid.item-has-focus) .item-native {
--background: var(--background-focused);
border-bottom-color: var(--highlight-color-focused);
}
:host(.item-fill-solid.item-shape-round) {
@@ -413,21 +471,3 @@
--border-color: #{$item-md-input-fill-border-color-hover};
}
}
// Material Design Item: Invalid
// --------------------------------------------------
:host(.item-fill-outline.ion-invalid:not(.ion-color)) .item-native,
:host(.item-fill-solid.ion-invalid:not(.ion-color)) .item-native {
caret-color: var(--highlight-color-invalid);
}
:host(.item-fill-outline.ion-invalid),
:host(.item-fill-outline.ion-invalid) .item-native,
:host(.item-fill-outline.ion-invalid:not(.ion-color)) .item-highlight,
:host(.item-fill-solid.ion-invalid:not(.ion-color)),
:host(.item-fill-solid.ion-invalid:not(.ion-color)) .item-native,
:host(.item-fill-solid.ion-invalid:not(.ion-color)) .item-highlight {
border-color: var(--highlight-color-invalid);
}

View File

@@ -313,10 +313,11 @@ button, a {
// to be clicked to open the select interface
::slotted(ion-label) {
pointer-events: none;
flex: 1;
}
::slotted(ion-label:not([slot="end"])) {
flex: 1;
}
// Item Input
// --------------------------------------------------
@@ -375,45 +376,10 @@ button, a {
pointer-events: none;
}
:host(.ion-focused) .item-highlight,
:host(.ion-focused) .item-inner-highlight,
:host(.item-has-focus) .item-highlight,
:host(.item-has-focus) .item-inner-highlight {
transform: scaleX(1);
border-style: var(--border-style);
border-color: var(--highlight-background);
:host(.item-interactive.item-has-focus) .item-native {
caret-color: var(--highlight-background);
}
:host(.ion-focused) .item-highlight,
:host(.item-has-focus) .item-highlight {
border-width: var(--full-highlight-height);
opacity: var(--show-full-highlight);
}
:host(.ion-focused) .item-inner-highlight,
:host(.item-has-focus) .item-inner-highlight {
border-bottom-width: var(--inset-highlight-height);
opacity: var(--show-inset-highlight);
}
:host(.ion-focused.item-fill-solid) .item-highlight,
:host(.item-has-focus.item-fill-solid) .item-highlight {
border-width: calc(var(--full-highlight-height) - 1px);
}
:host(.ion-focused) .item-inner-highlight,
:host(.ion-focused:not(.item-fill-outline)) .item-highlight,
:host(.item-has-focus) .item-inner-highlight,
:host(.item-has-focus:not(.item-fill-outline)) .item-highlight {
border-top: none;
border-right: none;
border-left: none;
}
// Item Input Focused
// --------------------------------------------------

View File

@@ -324,7 +324,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
Object.assign(childStyles, value);
});
const ariaDisabled = (disabled || childStyles['item-interactive-disabled']) ? 'true' : null;
const fillValue = fill || 'none';
return (
<Host
aria-disabled={ariaDisabled}
@@ -335,7 +335,7 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
'item': true,
[mode]: true,
[`item-lines-${lines}`]: lines !== undefined,
[`item-fill-${fill}`]: fill !== undefined,
[`item-fill-${fillValue}`]: true,
[`item-shape-${shape}`]: shape !== undefined,
'item-disabled': disabled,
'in-list': hostContext('ion-list', this.el),

View File

@@ -118,6 +118,25 @@
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>End Labels</ion-list-header>
<ion-item>
<ion-label slot="end">Time</ion-label>
<ion-datetime display-format="DDDD MMMM D YYYY hh:mm:ss a" value="2019-10-01T15:43:40.394Z"></ion-datetime>
</ion-item>
<ion-item>
<ion-label slot="end">From</ion-label>
<ion-input placeholder="Choose Starting Point"></ion-input>
</ion-item>
<ion-item>
<ion-label slot="end">Destination</ion-label>
<ion-select placeholder="Choose Really Really Long Destination Here">
<ion-select-option>Madison, WI</ion-select-option>
<ion-select-option>Atlanta, GA</ion-select-option>
</ion-select>
</ion-item>
</ion-list>
</ion-content>
</ion-app>

View File

@@ -0,0 +1,10 @@
import { newE2EPage } from '@stencil/core/testing';
test('item: form', async () => {
const page = await newE2EPage({
url: '/src/components/item/test/form?ionic:_testing=true'
});
const compare = await page.compareScreenshot();
expect(compare).toMatchScreenshot();
});

View File

@@ -0,0 +1,302 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Item - Form</title>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
.grid {
display: grid;
grid-template-columns: repeat(3, minmax(250px, 1fr));
grid-row-gap: 20px;
grid-column-gap: 20px;
}
h2 {
font-size: 12px;
font-weight: normal;
color: #6f7378;
margin-top: 10px;
margin-left: 5px;
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Item - Form</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<div class="grid">
<div class="grid-item">
<h2>No fill, Invalid, untouched</h2>
<ion-item class="ion-invalid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>No fill, Invalid, untouched, focused</h2>
<ion-item class="ion-invalid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>No fill, Invalid, touched</h2>
<ion-item class="ion-invalid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>No fill, Valid, untouched</h2>
<ion-item class="ion-valid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>No fill, Valid, untouched, focused</h2>
<ion-item class="ion-valid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>No fill, Valid, touched</h2>
<ion-item class="ion-valid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Outline, Invalid, untouched</h2>
<ion-item fill="outline" class="ion-invalid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Outline, Invalid, untouched, focused</h2>
<ion-item fill="outline" class="ion-invalid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Outline, Invalid, touched</h2>
<ion-item fill="outline" class="ion-invalid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Outline, Valid, untouched</h2>
<ion-item fill="outline" class="ion-valid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Outline, Valid, untouched, focused</h2>
<ion-item fill="outline" class="ion-valid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Outline, Valid, touched</h2>
<ion-item fill="outline" class="ion-valid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Solid, Invalid, untouched</h2>
<ion-item fill="solid" class="ion-invalid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Solid, Invalid, untouched, focused</h2>
<ion-item fill="solid" class="ion-invalid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Solid, Invalid, touched</h2>
<ion-item fill="solid" class="ion-invalid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Solid, Valid, untouched</h2>
<ion-item fill="solid" class="ion-valid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Solid, Valid, untouched, focused</h2>
<ion-item fill="solid" class="ion-valid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Solid, Valid, touched</h2>
<ion-item fill="solid" class="ion-valid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
</div>
<div class="grid">
<div class="grid-item">
<h2>Color, No fill, Invalid, untouched</h2>
<ion-item color="primary" class="ion-invalid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, No fill, Invalid, untouched, focused</h2>
<ion-item color="primary" class="ion-invalid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, No fill, Invalid, touched</h2>
<ion-item color="primary" class="ion-invalid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, No fill, Valid, untouched</h2>
<ion-item color="primary" class="ion-valid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, No fill, Valid, untouched, focused</h2>
<ion-item color="primary" class="ion-valid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, No fill, Valid, touched</h2>
<ion-item color="primary" class="ion-valid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Outline, Invalid, untouched</h2>
<ion-item color="primary" fill="outline" class="ion-invalid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Outline, Invalid, untouched, focused</h2>
<ion-item color="primary" fill="outline" class="ion-invalid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Outline, Invalid, touched</h2>
<ion-item color="primary" fill="outline" class="ion-invalid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Outline, Valid, untouched</h2>
<ion-item color="primary" fill="outline" class="ion-valid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Outline, Valid, untouched, focused</h2>
<ion-item color="primary" fill="outline" class="ion-valid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Outline, Valid, touched</h2>
<ion-item color="primary" fill="outline" class="ion-valid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Solid, Invalid, untouched</h2>
<ion-item color="primary" fill="solid" class="ion-invalid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Solid, Invalid, untouched, focused</h2>
<ion-item color="primary" fill="solid" class="ion-invalid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Solid, Invalid, touched</h2>
<ion-item color="primary" fill="solid" class="ion-invalid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Solid, Valid, untouched</h2>
<ion-item color="primary" fill="solid" class="ion-valid ion-untouched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Solid, Valid, untouched, focused</h2>
<ion-item color="primary" fill="solid" class="ion-valid ion-untouched item-has-focus">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
<div class="grid-item">
<h2>Color, Solid, Valid, touched</h2>
<ion-item color="primary" fill="solid" class="ion-valid ion-touched">
<ion-input placeholder="Text"></ion-input>
</ion-item>
</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -1,30 +1,43 @@
import { Animation } from '../../../interface';
import { Animation, ModalAnimationOptions } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { getElementRoot } from '../../../utils/helpers';
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
import { createSheetEnterAnimation } from './sheet';
const createEnterAnimation = () => {
const backdropAnimation = createAnimation()
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)');
const wrapperAnimation = createAnimation()
.fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
return { backdropAnimation, wrapperAnimation };
}
/**
* iOS Modal Enter Animation for the Card presentation style
*/
export const iosEnterAnimation = (
baseEl: HTMLElement,
presentingEl?: HTMLElement,
): Animation => {
baseEl: HTMLElement,
opts: ModalAnimationOptions,
): Animation => {
const { presentingEl, currentBreakpoint } = opts;
const root = getElementRoot(baseEl);
const backdropAnimation = createAnimation()
const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
backdropAnimation
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
.beforeStyles({
'pointer-events': 'none'
})
.afterClearStyles(['pointer-events']);
const wrapperAnimation = createAnimation()
wrapperAnimation
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(100vh)', 'translateY(0vh)');
.beforeStyles({ 'opacity': 1 });
const baseAnimation = createAnimation()
const baseAnimation = createAnimation('entering-base')
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(500)
@@ -48,7 +61,7 @@ export const iosEnterAnimation = (
/**
* Fallback for browsers that does not support `max()` (ex: Firefox)
* No need to worry about statusbar padding since engines like Gecko
* are not used as the engine for standlone Cordova/Capacitor apps
* are not used as the engine for standalone Cordova/Capacitor apps
*/
const transformOffset = (!CSS.supports('width', 'max(0px, 1px)')) ? '30px' : 'max(30px, var(--ion-safe-area-top))';
const modalTransform = hasCardModal ? '-10px' : transformOffset;

View File

@@ -1,27 +1,39 @@
import { Animation } from '../../../interface';
import { Animation, ModalAnimationOptions } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { getElementRoot } from '../../../utils/helpers';
import { SwipeToCloseDefaults } from '../gestures/swipe-to-close';
import { createSheetLeaveAnimation } from './sheet';
const createLeaveAnimation = () => {
const backdropAnimation = createAnimation()
.fromTo('opacity', 'var(--backdrop-opacity)', 0);
const wrapperAnimation = createAnimation()
.fromTo('transform', 'translateY(0vh)', 'translateY(100vh)');
return { backdropAnimation, wrapperAnimation };
}
/**
* iOS Modal Leave Animation
*/
export const iosLeaveAnimation = (
baseEl: HTMLElement,
presentingEl?: HTMLElement,
opts: ModalAnimationOptions,
duration = 500
): Animation => {
const { presentingEl, currentBreakpoint } = opts;
const root = getElementRoot(baseEl);
const backdropAnimation = createAnimation()
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0);
const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
const wrapperAnimation = createAnimation()
backdropAnimation.addElement(root.querySelector('ion-backdrop')!)
wrapperAnimation
.addElement(root.querySelectorAll('.modal-wrapper, .modal-shadow')!)
.beforeStyles({ 'opacity': 1 })
.fromTo('transform', 'translateY(0vh)', 'translateY(100vh)');
.beforeStyles({ 'opacity': 1 });
const baseAnimation = createAnimation()
const baseAnimation = createAnimation('leaving-base')
.addElement(baseEl)
.easing('cubic-bezier(0.32,0.72,0,1)')
.duration(duration)

View File

@@ -1,32 +1,44 @@
import { Animation } from '../../../interface';
import { Animation, ModalAnimationOptions } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { getElementRoot } from '../../../utils/helpers';
import { createSheetEnterAnimation } from './sheet';
const createEnterAnimation = () => {
const backdropAnimation = createAnimation()
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)');
const wrapperAnimation = createAnimation()
.keyframes([
{ offset: 0, opacity: 0.01, transform: 'translateY(40px)' },
{ offset: 1, opacity: 1, transform: `translateY(0px)` }
]);
return { backdropAnimation, wrapperAnimation };
}
/**
* Md Modal Enter Animation
*/
export const mdEnterAnimation = (baseEl: HTMLElement): Animation => {
export const mdEnterAnimation = (
baseEl: HTMLElement,
opts: ModalAnimationOptions
): Animation => {
const { currentBreakpoint } = opts;
const root = getElementRoot(baseEl);
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
const wrapperAnimation = createAnimation();
const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetEnterAnimation(opts) : createEnterAnimation();
backdropAnimation
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
.beforeStyles({
'pointer-events': 'none'
})
.afterClearStyles(['pointer-events']);
wrapperAnimation
.addElement(root.querySelector('.modal-wrapper')!)
.keyframes([
{ offset: 0, opacity: 0.01, transform: 'translateY(40px)' },
{ offset: 1, opacity: 1, transform: 'translateY(0px)' }
]);
.addElement(root.querySelector('.modal-wrapper')!);
return baseAnimation
return createAnimation()
.addElement(baseEl)
.easing('cubic-bezier(0.36,0.66,0.04,1)')
.duration(280)

View File

@@ -1,30 +1,37 @@
import { Animation } from '../../../interface';
import { Animation, ModalAnimationOptions } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { getElementRoot } from '../../../utils/helpers';
import { createSheetLeaveAnimation } from './sheet';
const createLeaveAnimation = () => {
const backdropAnimation = createAnimation()
.fromTo('opacity', 'var(--backdrop-opacity)', 0);
const wrapperAnimation = createAnimation()
.keyframes([
{ offset: 0, opacity: 0.99, transform: `translateY(0px)` },
{ offset: 1, opacity: 0, transform: 'translateY(40px)' }
]);
return { backdropAnimation, wrapperAnimation };
}
/**
* Md Modal Leave Animation
*/
export const mdLeaveAnimation = (baseEl: HTMLElement): Animation => {
export const mdLeaveAnimation = (
baseEl: HTMLElement,
opts: ModalAnimationOptions
): Animation => {
const { currentBreakpoint } = opts;
const root = getElementRoot(baseEl);
const baseAnimation = createAnimation();
const backdropAnimation = createAnimation();
const wrapperAnimation = createAnimation();
const wrapperEl = root.querySelector('.modal-wrapper')!;
const { wrapperAnimation, backdropAnimation } = currentBreakpoint !== undefined ? createSheetLeaveAnimation(opts) : createLeaveAnimation();
backdropAnimation
.addElement(root.querySelector('ion-backdrop')!)
.fromTo('opacity', 'var(--backdrop-opacity)', 0.0);
backdropAnimation.addElement(root.querySelector('ion-backdrop')!);
wrapperAnimation.addElement(root.querySelector('.modal-wrapper')!);
wrapperAnimation
.addElement(wrapperEl)
.keyframes([
{ offset: 0, opacity: 0.99, transform: 'translateY(0px)' },
{ offset: 1, opacity: 0, transform: 'translateY(40px)' }
]);
return baseAnimation
.addElement(baseEl)
return createAnimation()
.easing('cubic-bezier(0.47,0,0.745,0.715)')
.duration(200)
.addAnimation([backdropAnimation, wrapperAnimation]);

View File

@@ -0,0 +1,59 @@
import { ModalAnimationOptions } from '../../../interface';
import { createAnimation } from '../../../utils/animation/animation';
import { getBackdropValueForSheet } from '../utils';
export const createSheetEnterAnimation = (opts: ModalAnimationOptions) => {
const { currentBreakpoint, backdropBreakpoint } = opts;
/**
* If the backdropBreakpoint is undefined, then the backdrop
* should always fade in. If the backdropBreakpoint came before the
* current breakpoint, then the backdrop should be fading in.
*/
const shouldShowBackdrop = backdropBreakpoint === undefined || backdropBreakpoint < currentBreakpoint!;
const initialBackdrop = shouldShowBackdrop ? `calc(var(--backdrop-opacity) * ${currentBreakpoint!})` : '0.01';
const backdropAnimation = createAnimation('backdropAnimation')
.fromTo('opacity', 0, initialBackdrop);
const wrapperAnimation = createAnimation('wrapperAnimation')
.keyframes([
{ offset: 0, opacity: 1, transform: 'translateY(100%)' },
{ offset: 1, opacity: 1, transform: `translateY(${100 - (currentBreakpoint! * 100)}%)` }
]);
return { wrapperAnimation, backdropAnimation };
}
export const createSheetLeaveAnimation = (opts: ModalAnimationOptions) => {
const { currentBreakpoint, backdropBreakpoint, sortedBreakpoints } = opts;
/**
* Backdrop does not always fade in from 0 to 1 if backdropBreakpoint
* is defined, so we need to account for that offset by figuring out
* what the current backdrop value should be.
*/
const maxBreakpoint = sortedBreakpoints![sortedBreakpoints.length - 1];
const backdropValue = `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(currentBreakpoint!, maxBreakpoint, backdropBreakpoint!)})`;
const defaultBackdrop = [
{ offset: 0, opacity: backdropValue },
{ offset: 1, opacity: 0 }
]
const customBackdrop = [
{ offset: 0, opacity: backdropValue },
{ offset: backdropBreakpoint!, opacity: 0 },
{ offset: 1, opacity: 0 }
]
const backdropAnimation = createAnimation('backdropAnimation')
.keyframes(backdropBreakpoint !== 0 ? customBackdrop : defaultBackdrop);
const wrapperAnimation = createAnimation('wrapperAnimation')
.keyframes([
{ offset: 0, opacity: 1, transform: `translateY(${100 - (currentBreakpoint! * 100)}%)` },
{ offset: 1, opacity: 1, transform: `translateY(100%)` }
]);
return { wrapperAnimation, backdropAnimation };
}

View File

@@ -0,0 +1,208 @@
import { Animation } from '../../../interface';
import { GestureDetail, createGesture } from '../../../utils/gesture';
import { clamp, raf } from '../../../utils/helpers';
import { getBackdropValueForSheet } from '../utils';
export const createSheetGesture = (
baseEl: HTMLIonModalElement,
backdropEl: HTMLIonBackdropElement,
wrapperEl: HTMLElement,
initialBreakpoint: number,
backdropBreakpoint: number,
animation: Animation,
breakpoints: number[] = [],
onDismiss: () => void,
onBreakpointChange: (breakpoint: number) => void
) => {
// Defaults for the sheet swipe animation
const defaultBackdrop = [
{ offset: 0, opacity: 'var(--backdrop-opacity)' },
{ offset: 1, opacity: 0.01 }
]
const customBackdrop = [
{ offset: 0, opacity: 'var(--backdrop-opacity)' },
{ offset: backdropBreakpoint, opacity: 0 },
{ offset: 1, opacity: 0 }
]
const SheetDefaults = {
WRAPPER_KEYFRAMES: [
{ offset: 0, transform: 'translateY(0%)' },
{ offset: 1, transform: 'translateY(100%)' }
],
BACKDROP_KEYFRAMES: (backdropBreakpoint !== 0) ? customBackdrop : defaultBackdrop
};
const contentEl = baseEl.querySelector('ion-content');
const height = wrapperEl.clientHeight;
let currentBreakpoint = initialBreakpoint;
let offset = 0;
const wrapperAnimation = animation.childAnimations.find(ani => ani.id === 'wrapperAnimation');
const backdropAnimation = animation.childAnimations.find(ani => ani.id === 'backdropAnimation');
const maxBreakpoint = breakpoints[breakpoints.length - 1];
/**
* After the entering animation completes,
* we need to set the animation to go from
* offset 0 to offset 1 so that users can
* swipe in any direction. We then set the
* animation offset to the current breakpoint
* so there is no flickering.
*/
if (wrapperAnimation && backdropAnimation) {
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
animation.progressStart(true, 1 - currentBreakpoint);
const backdropEnabled = currentBreakpoint >= backdropBreakpoint
backdropEl.style.setProperty('pointer-events', backdropEnabled ? 'auto' : 'none');
}
if (contentEl && currentBreakpoint !== maxBreakpoint) {
contentEl.scrollY = false;
}
const canStart = (detail: GestureDetail) => {
/**
* If the sheet is fully expanded and
* the user is swiping on the content,
* the gesture should not start to
* allow for scrolling on the content.
*/
const content = (detail.event.target! as HTMLElement).closest('ion-content');
if (currentBreakpoint === 1 && content) {
return false;
}
return true;
};
const onStart = () => {
/**
* If swiping on the content
* we should disable scrolling otherwise
* the sheet will expand and the content will scroll.
*/
if (contentEl) {
contentEl.scrollY = false;
}
animation.progressStart(true, 1 - currentBreakpoint);
};
const onMove = (detail: GestureDetail) => {
/**
* Given the change in gesture position on the Y axis,
* compute where the offset of the animation should be
* relative to where the user dragged.
*/
const initialStep = 1 - currentBreakpoint;
offset = clamp(0.0001, initialStep + (detail.deltaY / height), 0.9999);
animation.progressStep(offset);
};
const onEnd = (detail: GestureDetail) => {
/**
* When the gesture releases, we need to determine
* the closest breakpoint to snap to.
*/
const velocity = detail.velocityY;
const threshold = (detail.deltaY + velocity * 100) / height;
const diff = currentBreakpoint - threshold;
const closest = breakpoints.reduce((a, b) => {
return Math.abs(b - diff) < Math.abs(a - diff) ? b : a;
});
const shouldRemainOpen = closest !== 0;
currentBreakpoint = 0;
/**
* Update the animation so that it plays from
* the last offset to the closest snap point.
*/
if (wrapperAnimation && backdropAnimation) {
wrapperAnimation.keyframes([
{ offset: 0, transform: `translateY(${offset * 100}%)` },
{ offset: 1, transform: `translateY(${(1 - closest) * 100}%)` }
]);
backdropAnimation.keyframes([
{ offset: 0, opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(1 - offset, maxBreakpoint, backdropBreakpoint)})` },
{ offset: 1, opacity: `calc(var(--backdrop-opacity) * ${getBackdropValueForSheet(closest, maxBreakpoint, backdropBreakpoint)})` }
]);
animation.progressStep(0);
}
/**
* Gesture should remain disabled until the
* snapping animation completes.
*/
gesture.enable(false);
animation
.onFinish(() => {
if (shouldRemainOpen) {
/**
* Once the snapping animation completes,
* we need to reset the animation to go
* from 0 to 1 so users can swipe in any direction.
* We then set the animation offset to the current
* breakpoint so that it starts at the snapped position.
*/
if (wrapperAnimation && backdropAnimation) {
raf(() => {
wrapperAnimation.keyframes([...SheetDefaults.WRAPPER_KEYFRAMES]);
backdropAnimation.keyframes([...SheetDefaults.BACKDROP_KEYFRAMES]);
animation.progressStart(true, 1 - closest);
currentBreakpoint = closest;
onBreakpointChange(currentBreakpoint);
/**
* If the sheet is fully expanded, we can safely
* enable scrolling again.
*/
if (contentEl && currentBreakpoint === breakpoints[breakpoints.length - 1]) {
contentEl.scrollY = true;
}
const backdropEnabled = currentBreakpoint >= backdropBreakpoint;
backdropEl.style.setProperty('pointer-events', backdropEnabled ? 'auto' : 'none');
gesture.enable(true);
});
} else {
gesture.enable(true);
}
}
/**
* This must be a one time callback
* otherwise a new callback will
* be added every time onEnd runs.
*/
}, { oneTimeCallback: true })
.progressEnd(1, 0, 500);
if (!shouldRemainOpen) {
onDismiss();
}
};
const gesture = createGesture({
el: wrapperEl,
gestureName: 'modalSheet',
gesturePriority: 40,
direction: 'y',
threshold: 10,
canStart,
onStart,
onMove,
onEnd
});
return gesture;
};

View File

@@ -18,3 +18,10 @@ export interface ModalOptions<T extends ComponentRef = ComponentRef> {
enterAnimation?: AnimationBuilder;
leaveAnimation?: AnimationBuilder;
}
export interface ModalAnimationOptions {
presentingEl?: HTMLElement;
currentBreakpoint?: number;
backdropBreakpoint?: number;
sortedBreakpoints: number[];
}

View File

@@ -19,6 +19,9 @@
@include transform(translate3d(0, 100%, 0));
}
// iOS Card Modal
// --------------------------------------------------
@media screen and (max-width: 767px) {
@supports (width: max(0px, 1px)) {
:host(.modal-card) {
@@ -68,3 +71,10 @@
box-shadow: var(--box-shadow);
}
}
// iOS Sheet Modal
// --------------------------------------------------
:host(.modal-sheet) .modal-wrapper {
@include border-radius($modal-ios-border-radius, $modal-ios-border-radius, 0, 0);
}

View File

@@ -65,6 +65,9 @@
.modal-shadow {
@include border-radius(var(--border-radius));
position: absolute;
bottom: 0;
width: var(--width);
min-width: var(--min-width);
max-width: var(--max-width);
@@ -107,3 +110,35 @@
--height: #{$modal-inset-height-large};
}
}
// Sheet Modal
// --------------------------------------------------
.modal-handle {
@include position(14px, 0px, null, 0px);
@include border-radius(8px, 8px, 8px, 8px);
@include margin(null, auto, null, auto);
position: absolute;
width: 36px;
height: 5px;
/**
* This allows the handle to appear
* on top of user content in WebKit.
*/
transform: translateZ(0);
background: var(--ion-color-step-350, #c0c0be);
z-index: 11;
}
/**
* Ensure that the sheet modal does not
* completely cover the content.
*/
:host(.modal-sheet) {
--height: calc(100% - 10px);
}

View File

@@ -13,6 +13,7 @@ import { iosEnterAnimation } from './animations/ios.enter';
import { iosLeaveAnimation } from './animations/ios.leave';
import { mdEnterAnimation } from './animations/md.enter';
import { mdLeaveAnimation } from './animations/md.leave';
import { createSheetGesture } from './gestures/sheet';
import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
/**
@@ -22,6 +23,7 @@ import { createSwipeToCloseGesture } from './gestures/swipe-to-close';
*
* @part backdrop - The `ion-backdrop` element.
* @part content - The wrapper element for the default slot.
* @part handle - The handle that is displayed at the top of the sheet modal when `handle="true"`.
*/
@Component({
tag: 'ion-modal',
@@ -38,6 +40,11 @@ export class Modal implements ComponentInterface, OverlayInterface {
private coreDelegate: FrameworkDelegate = CoreDelegate();
private currentTransition?: Promise<any>;
private destroyTriggerInteraction?: () => void;
private isSheetModal = false;
private currentBreakpoint?: number;
private wrapperEl?: HTMLElement;
private backdropEl?: HTMLIonBackdropElement;
private sortedBreakpoints: number[] = [];
private inline = false;
private workingDelegate?: FrameworkDelegate;
@@ -75,6 +82,40 @@ export class Modal implements ComponentInterface, OverlayInterface {
*/
@Prop() leaveAnimation?: AnimationBuilder;
/**
* The breakpoints to use when creating a sheet modal. Each value in the
* array must be a decimal between 0 and 1 where 0 indicates the modal is fully
* closed and 1 indicates the modal is fully open. Values are relative
* to the height of the modal, not the height of the screen. One of the values in this
* array must be the value of the `initialBreakpoint` property.
* For example: [0, .25, .5, 1]
*/
@Prop() breakpoints?: number[];
/**
* A decimal value between 0 and 1 that indicates the
* initial point the modal will open at when creating a
* sheet modal. This value must also be listed in the
* `breakpoints` array.
*/
@Prop() initialBreakpoint?: number;
/**
* A decimal value between 0 and 1 that indicates the
* point at which the backdrop will begin to fade in
* when using a sheet modal. Prior to this point, the
* backdrop will be hidden and the content underneath
* the sheet can be interacted with. This value must
* also be listed in the `breakpoints` array.
*/
@Prop() backdropBreakpoint = 0;
/**
* The horizontal line that displays at the top of a sheet modal. It is `true` by default when
* setting the `breakpoints` and `initialBreakpoint` properties.
*/
@Prop() handle?: boolean;
/**
* The component to display inside of the modal.
* @internal
@@ -206,11 +247,18 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
componentWillLoad() {
const { breakpoints, initialBreakpoint } = this;
/**
* If user has custom ID set then we should
* not assign the default incrementing ID.
*/
this.modalId = (this.el.hasAttribute('id')) ? this.el.getAttribute('id')! : `ion-modal-${this.modalIndex}`;
this.isSheetModal = breakpoints !== undefined && initialBreakpoint !== undefined;
if (breakpoints !== undefined && initialBreakpoint !== undefined && !breakpoints.includes(initialBreakpoint)) {
console.warn('[Ionic Warning]: Your breakpoints array must include the initialBreakpoint value.')
}
}
componentDidLoad() {
@@ -315,15 +363,17 @@ export class Modal implements ComponentInterface, OverlayInterface {
writeTask(() => this.el.classList.add('show-modal'));
this.currentTransition = present(this, 'modalEnter', iosEnterAnimation, mdEnterAnimation, this.presentingElement);
this.currentTransition = present(this, 'modalEnter', iosEnterAnimation, mdEnterAnimation, { presentingEl: this.presentingElement, currentBreakpoint: this.initialBreakpoint, backdropBreakpoint: this.backdropBreakpoint });
await this.currentTransition;
this.currentTransition = undefined;
if (this.swipeToClose) {
if (this.isSheetModal) {
this.initSheetGesture();
} else if (this.swipeToClose) {
this.initSwipeToClose();
}
this.currentTransition = undefined;
}
private initSwipeToClose() {
@@ -333,7 +383,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
// should be in the DOM and referenced by now, except
// for the presenting el
const animationBuilder = this.leaveAnimation || config.get('modalLeave', iosLeaveAnimation);
const ani = this.animation = animationBuilder(this.el, this.presentingElement);
const ani = this.animation = animationBuilder(this.el, { presentingEl: this.presentingElement });
this.gesture = createSwipeToCloseGesture(
this.el,
ani,
@@ -354,6 +404,53 @@ export class Modal implements ComponentInterface, OverlayInterface {
this.gestureAnimationDismissing = false;
});
},
);
this.gesture.enable(true);
}
private initSheetGesture() {
const { wrapperEl, initialBreakpoint, backdropBreakpoint } = this;
if (!wrapperEl || initialBreakpoint === undefined) {
return;
}
const animationBuilder = this.enterAnimation || config.get('modalEnter', iosEnterAnimation);
const ani: Animation = this.animation = animationBuilder(this.el, { presentingEl: this.presentingElement, currentBreakpoint: initialBreakpoint, backdropBreakpoint });
ani.progressStart(true, 1);
const sortedBreakpoints = this.sortedBreakpoints = (this.breakpoints?.sort((a, b) => a - b)) || [];
this.gesture = createSheetGesture(
this.el,
this.backdropEl!,
wrapperEl,
initialBreakpoint,
backdropBreakpoint,
ani,
sortedBreakpoints,
() => {
/**
* While the gesture animation is finishing
* it is possible for a user to tap the backdrop.
* This would result in the dismiss animation
* being played again. Typically this is avoided
* by setting `presented = false` on the overlay
* component; however, we cannot do that here as
* that would prevent the element from being
* removed from the DOM.
*/
this.gestureAnimationDismissing = true;
this.animation!.onFinish(async () => {
await this.dismiss(undefined, 'gesture');
this.gestureAnimationDismissing = false;
});
},
(breakpoint: number) => {
this.currentBreakpoint = breakpoint;
}
);
this.gesture.enable(true);
}
@@ -384,7 +481,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
const enteringAnimation = activeAnimations.get(this) || [];
this.currentTransition = dismiss(this, data, role, 'modalLeave', iosLeaveAnimation, mdLeaveAnimation, this.presentingElement);
this.currentTransition = dismiss(this, data, role, 'modalLeave', iosLeaveAnimation, mdLeaveAnimation, { presentingEl: this.presentingElement, currentBreakpoint: this.currentBreakpoint || this.initialBreakpoint, sortedBreakpoints: this.sortedBreakpoints, backdropBreakpoint: this.backdropBreakpoint });
const dismissed = await this.currentTransition;
@@ -394,12 +491,15 @@ export class Modal implements ComponentInterface, OverlayInterface {
if (this.animation) {
this.animation.destroy();
}
if (this.gesture) {
this.gesture.destroy();
}
enteringAnimation.forEach(ani => ani.destroy());
}
this.animation = undefined;
this.currentTransition = undefined;
this.animation = undefined;
return dismissed;
}
@@ -445,6 +545,10 @@ export class Modal implements ComponentInterface, OverlayInterface {
}
render() {
const { handle, isSheetModal, presentingElement } = this;
const showHandle = handle || isSheetModal;
const mode = getIonMode(this);
const { presented, modalId } = this;
@@ -455,7 +559,8 @@ export class Modal implements ComponentInterface, OverlayInterface {
tabindex="-1"
class={{
[mode]: true,
[`modal-card`]: this.presentingElement !== undefined && mode === 'ios',
[`modal-card`]: presentingElement !== undefined && mode === 'ios',
[`modal-sheet`]: isSheetModal,
'overlay-hidden': true,
'modal-interactive': presented,
...getClassMap(this.cssClass)
@@ -471,7 +576,7 @@ export class Modal implements ComponentInterface, OverlayInterface {
onIonModalWillDismiss={this.onLifecycle}
onIonModalDidDismiss={this.onLifecycle}
>
<ion-backdrop visible={this.showBackdrop} tappable={this.backdropDismiss} part="backdrop" />
<ion-backdrop ref={el => this.backdropEl = el} visible={this.showBackdrop} tappable={this.backdropDismiss} part="backdrop" />
{mode === 'ios' && <div class="modal-shadow"></div>}
@@ -479,7 +584,9 @@ export class Modal implements ComponentInterface, OverlayInterface {
role="dialog"
class="modal-wrapper ion-overlay-wrapper"
part="content"
ref={el => this.wrapperEl = el}
>
{showHandle && <div class="modal-handle" part="handle"></div>}
<slot></slot>
</div>

View File

@@ -38,6 +38,30 @@ If you need fine grained control over when the modal is presented and dismissed,
We typically recommend that you write your modals inline as it streamlines the amount of code in your application. You should only use the `modalController` for complex use cases where writing a modal inline is impractical.
## Card Modal
Developers can create a card modal effect where the modal appears as a card stacked on top of your app's main content. To create a card modal, developers need to set the `presentingElement` property and the `swipeToClose` properties on `ion-modal`.
The `presentingElement` property accepts a reference to the element that should display under your modal. This is typically a reference to `ion-router-outlet`.
The `swipeToClose` property can be used to control whether or not the card modal can be swiped to close.
See [Usage](#usage) for examples on how to use the sheet modal.
## Sheet Modal
Developers can create a sheet modal effect similar to the drawer components available in maps applications. To create a sheet modal, developers need to set the `breakpoints` and `initialBreakpoint` properties on `ion-modal`.
The `breakpoints` property accepts an array which states each breakpoint that the sheet can snap to when swiped. A `breakpoints` property of `[0, 0.5, 1]` would indicate that the sheet can be swiped to 0% of the screen height, 50% of the screen height, and 100% of the screen height. When the modal is swiped to 0% of the screen height, the modal will be automatically dismissed.
The `initialBreakpoint` property is required so that the sheet modal knows which breakpoint to start at when presenting. The `initalBreakpoint` value must also exist in the `breakpoints` array. Given a `breakpoints` value of `[0, 0.5, 1]`, an `initialBreakpoint` value of `0.5` would be valid as `0.5` is in the `breakpoints` array. An `initialBreakpoint` value of `0.25` would not be valid as `0.25` does not exist in the `breakpoints` array.
The `backdropBreakpoint` property can be used to customize the point at which the `ion-backdrop` will begin to fade in. This is useful when creating interfaces that have content underneath the sheet that should remain interactive. A common use case is a sheet modal that overlays a map where the map is interactive until the sheet is fully expanded.
See [Usage](#usage) for examples on how to use the sheet modal.
> Note: The `swipeToClose` property has no effect when using a sheet modal as sheet modals must be swipeable in order to be usable.
## Interfaces
Below you will find all of the options available to you when using the `modalController`. These options should be supplied when calling `modalController.create()`.
@@ -264,7 +288,7 @@ import { EventModalModule } from '../modals/event/event.module';
export class CalendarComponentModule {}
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -306,6 +330,34 @@ async presentModal() {
}
```
### Sheet Modals
**Controller**
```javascript
import { IonRouterOutlet } from '@ionic/angular';
constructor(private routerOutlet: IonRouterOutlet) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalPage,
initialBreakpoint: 0.5,
breakpoints: [0, 0.5, 1]
});
return await modal.present();
}
```
**Inline**
```html
<ion-modal [isOpen]="isModalOpen" [initialBreakpoint]="0.5" [breakpoints]="[0, 0.5, 1]">
<ng-template>
<modal-page></modal-page>
</ng-template>
</ion-modal>
```
### Style Placement
@@ -397,7 +449,7 @@ console.log(data);
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -421,6 +473,15 @@ modalElement.swipeToClose = true;
modalElement.presentingElement = await modalController.getTop(); // Get the top-most ion-modal
```
### Sheet Modals
```javascript
const modalElement = document.createElement('ion-modal');
modalElement.component = 'modal-page';
modalElement.initialBreakpoint = 0.5;
modalElement.breakpoints = [0, 0.5, 1];
```
### React
@@ -507,7 +568,7 @@ export const ModalExample: React.FC = () => {
};
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -538,18 +599,18 @@ const Home: React.FC<HomePageProps> = ({ router }) => {
const [showModal, setShowModal] = useState(false);
return (
...
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
...
<IonPage>
<IonContent>
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
</IonContent>
</IonPage>
);
};
@@ -581,6 +642,46 @@ In most scenarios, setting a ref on `IonRouterOutlet` and passing that ref's `cu
```
### Sheet Modals
```tsx
const App: React.FC = () => {
const routerRef = useRef<HTMLIonRouterOutletElement | null>(null);
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet ref={routerRef}>
<Route path="/home" render={() => <Home router={routerRef.current} />} exact={true} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
};
...
const Home: React.FC = () => {
const [showModal, setShowModal] = useState(false);
return (
<IonPage>
<IonContent>
<IonModal
isOpen={showModal}
initialBreakpoint={0.5}
breakpoints={[0, 0.5, 1]}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
</IonContent>
</IonPage>
);
};
```
### Stencil
```tsx
@@ -692,7 +793,7 @@ const { data } = await modal.onWillDismiss();
console.log(data);
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -738,6 +839,59 @@ async presentModal() {
```
### Sheet Modals
**Controller**
```tsx
import { Component, Element, h } from '@stencil/core';
import { modalController } from '@ionic/core';
@Component({
tag: 'modal-example',
styleUrl: 'modal-example.css'
})
export class ModalExample {
@Element() el: any;
async presentModal() {
const modal = await modalController.create({
component: 'page-modal',
initialBreakpoint: 0.5,
breakpoints: [0, 0.5, 1]
});
await modal.present();
}
}
```
**Inline**
```tsx
import { Component, State, h } from '@stencil/core';
@Component({
tag: 'modal-example',
styleUrl: 'modal-example.css'
})
export class ModalExample {
@State() isModalOpen: boolean = false;
render() {
return [
<ion-modal
isOpen={isModalOpen}
initialBreakpoint={0.5}
breakpoints={[0, 0.5, 1]}
>
<page-modal></page-modal>
<ion-modal>
]
}
}
```
### Vue
```html
@@ -836,7 +990,7 @@ export default defineComponent({
> If you need a wrapper element inside of your modal component, we recommend using an `<ion-page>` so that the component dimensions are still computed properly.
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -877,23 +1031,94 @@ export default defineComponent({
</script>
```
### Sheet Modals
**Controller**
```html
<template>
<ion-page>
<ion-content class="ion-padding">
<ion-button @click="openModal()">Open Modal</ion-button>
</ion-content>
</ion-page>
</template>
<script>
import { IonButton, IonContent, IonPage, modalController } from '@ionic/vue';
import Modal from './modal.vue'
export default {
components: { IonButton, IonContent, IonPage },
methods: {
async openModal() {
const modal = await modalController
.create({
component: Modal,
initialBreakpoint: 0.5,
breakpoints: [0, 0.5, 1]
})
return modal.present();
},
},
}
</script>
```
**Inline**
```html
<template>
<ion-page>
<ion-content>
<ion-button @click="setOpen(true)">Show Modal</ion-button>
<ion-modal
:is-open="isOpenRef"
:initial-breakpoint="0.5"
:breakpoints="[0, 0.5, 1]"
@didDismiss="setOpen(false)"
>
<Modal></Modal>
</ion-modal>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { IonModal, IonButton, IonContent, IonPage } from '@ionic/vue';
import { defineComponent, ref } from 'vue';
import Modal from './modal.vue'
export default defineComponent({
components: { IonModal, IonButton, Modal, IonContent, IonPage },
setup() {
const isOpenRef = ref(false);
const setOpen = (state: boolean) => isOpenRef.value = state;
return { isOpenRef, setOpen }
}
});
</script>
```
## Properties
| Property | Attribute | Description | Type | Default |
| ------------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- |
| `animated` | `animated` | If `true`, the modal will animate. | `boolean` | `true` |
| `backdropDismiss` | `backdrop-dismiss` | If `true`, the modal will be dismissed when the backdrop is clicked. | `boolean` | `true` |
| `enterAnimation` | -- | Animation to use when the modal is presented. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `isOpen` | `is-open` | If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code. | `boolean` | `false` |
| `keyboardClose` | `keyboard-close` | If `true`, the keyboard will be automatically dismissed when the overlay is presented. | `boolean` | `true` |
| `leaveAnimation` | -- | Animation to use when the modal is dismissed. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `presentingElement` | -- | The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode. | `HTMLElement \| undefined` | `undefined` |
| `showBackdrop` | `show-backdrop` | If `true`, a backdrop will be displayed behind the modal. | `boolean` | `true` |
| `swipeToClose` | `swipe-to-close` | If `true`, the modal can be swiped to dismiss. Only applies in iOS mode. | `boolean` | `false` |
| `trigger` | `trigger` | An ID corresponding to the trigger element that causes the modal to open when clicked. | `string \| undefined` | `undefined` |
| Property | Attribute | Description | Type | Default |
| -------------------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------- |
| `animated` | `animated` | If `true`, the modal will animate. | `boolean` | `true` |
| `backdropBreakpoint` | `backdrop-breakpoint` | A decimal value between 0 and 1 that indicates the point at which the backdrop will begin to fade in when using a sheet modal. Prior to this point, the backdrop will be hidden and the content underneath the sheet can be interacted with. This value must also be listed in the `breakpoints` array. | `number` | `0` |
| `backdropDismiss` | `backdrop-dismiss` | If `true`, the modal will be dismissed when the backdrop is clicked. | `boolean` | `true` |
| `breakpoints` | -- | The breakpoints to use when creating a sheet modal. Each value in the array must be a decimal between 0 and 1 where 0 indicates the modal is fully closed and 1 indicates the modal is fully open. Values are relative to the height of the modal, not the height of the screen. One of the values in this array must be the value of the `initialBreakpoint` property. For example: [0, .25, .5, 1] | `number[] \| undefined` | `undefined` |
| `enterAnimation` | -- | Animation to use when the modal is presented. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `handle` | `handle` | The horizontal line that displays at the top of a sheet modal. It is `true` by default when setting the `breakpoints` and `initialBreakpoint` properties. | `boolean \| undefined` | `undefined` |
| `initialBreakpoint` | `initial-breakpoint` | A decimal value between 0 and 1 that indicates the initial point the modal will open at when creating a sheet modal. This value must also be listed in the `breakpoints` array. | `number \| undefined` | `undefined` |
| `isOpen` | `is-open` | If `true`, the modal will open. If `false`, the modal will close. Use this if you need finer grained control over presentation, otherwise just use the modalController or the `trigger` property. Note: `isOpen` will not automatically be set back to `false` when the modal dismisses. You will need to do that in your code. | `boolean` | `false` |
| `keyboardClose` | `keyboard-close` | If `true`, the keyboard will be automatically dismissed when the overlay is presented. | `boolean` | `true` |
| `leaveAnimation` | -- | Animation to use when the modal is dismissed. | `((baseEl: any, opts?: any) => Animation) \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `undefined` |
| `presentingElement` | -- | The element that presented the modal. This is used for card presentation effects and for stacking multiple modals on top of each other. Only applies in iOS mode. | `HTMLElement \| undefined` | `undefined` |
| `showBackdrop` | `show-backdrop` | If `true`, a backdrop will be displayed behind the modal. | `boolean` | `true` |
| `swipeToClose` | `swipe-to-close` | If `true`, the modal can be swiped to dismiss. Only applies in iOS mode. | `boolean` | `false` |
| `trigger` | `trigger` | An ID corresponding to the trigger element that causes the modal to open when clicked. | `string \| undefined` | `undefined` |
## Events
@@ -962,10 +1187,11 @@ Type: `Promise<void>`
## Shadow Parts
| Part | Description |
| ------------ | ----------------------------------------- |
| `"backdrop"` | The `ion-backdrop` element. |
| `"content"` | The wrapper element for the default slot. |
| Part | Description |
| ------------ | -------------------------------------------------------------------------------- |
| `"backdrop"` | The `ion-backdrop` element. |
| `"content"` | The wrapper element for the default slot. |
| `"handle"` | The handle that is displayed at the top of the sheet modal when `handle="true"`. |
## CSS Custom Properties

View File

@@ -0,0 +1,45 @@
import { newE2EPage } from '@stencil/core/testing';
import { testModal } from '../test.utils';
const DIRECTORY = 'sheet';
test('modal: sheet', async () => {
await testModal(DIRECTORY, '#sheet-modal');
});
test('modal:rtl: sheet', async () => {
await testModal(DIRECTORY, '#sheet-modal', true);
});
test.only('modal - open', async () => {
const screenshotCompares = [];
const page = await newE2EPage({ url: '/src/components/modal/test/sheet?ionic:_testing=true' });
await page.click('#sheet-modal');
const modal = await page.find('ion-modal');
await modal.waitForVisible();
screenshotCompares.push(await page.compareScreenshot());
await modal.callMethod('dismiss');
await modal.waitForNotVisible();
screenshotCompares.push(await page.compareScreenshot('dismiss'));
await page.click('#sheet-modal');
const modalAgain = await page.find('ion-modal');
await modalAgain.waitForVisible();
screenshotCompares.push(await page.compareScreenshot());
await modalAgain.callMethod('dismiss');
await modalAgain.waitForNotVisible();
screenshotCompares.push(await page.compareScreenshot('dismiss'));
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});

View File

@@ -0,0 +1,181 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Modal - Sheet</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link href="../../../../../css/ionic.bundle.css" rel="stylesheet">
<link href="../../../../../scripts/testing/styles.css" rel="stylesheet">
<script src="../../../../../scripts/testing/scripts.js"></script>
<script nomodule src="../../../../../dist/ionic/ionic.js"></script>
<script type="module" src="../../../../../dist/ionic/ionic.esm.js"></script>
<style>
:root {
--ion-safe-area-top: 20px;
--ion-safe-area-bottom: 20px;
}
.custom-height {
--height: 50%;
}
.custom-handle::part(handle) {
top: -16px;
background: rgba(255, 255, 255, 0.53);
}
.custom-handle::part(content) {
overflow: visible;
}
.red {
background-color: #ea445a;
}
.green {
background-color: #76d672;
}
.blue {
background-color: #3478f6;
}
.yellow {
background-color: #ffff80;
}
.pink {
background-color: #ff6b86;
}
.purple {
background-color: #7e34f6;
}
.black {
background-color: #000;
}
.orange {
background-color: #f69234;
}
.grid {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 10px;
padding: 10px;
}
.grid-item {
height: 200px;
}
</style>
</head>
<body>
<ion-app>
<div class="ion-page">
<ion-header>
<ion-toolbar>
<ion-title>Modal - Sheet</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<ion-button id="sheet-modal" onclick="presentModal()">Present Sheet Modal</ion-button>
<ion-button id="sheet-modal" onclick="presentModal({ initialBreakpoint: 0.5, breakpoints: [0, 0.5, 1] })">Present Sheet Modal (Custom Breakpoints)</ion-button>
<ion-button id="sheet-modal" onclick="presentModal({ backdropBreakpoint: 0.5 })">Present Sheet Modal (Custom Backdrop Breakpoint)</ion-button>
<ion-button id="sheet-modal" onclick="presentModal({ cssClass: 'custom-height' })">Present Sheet Modal (Custom Height)</ion-button>
<ion-button id="sheet-modal" onclick="presentModal({ cssClass: 'custom-handle' })">Present Sheet Modal (Custom Handle)</ion-button>
<div class="grid">
<div class="grid-item red"></div>
<div class="grid-item green"></div>
<div class="grid-item blue"></div>
<div class="grid-item yellow"></div>
<div class="grid-item pink"></div>
<div class="grid-item purple"></div>
<div class="grid-item black"></div>
<div class="grid-item orange"></div>
</div>
</ion-content>
</div>
</ion-app>
<script>
window.addEventListener("ionModalDidDismiss", function (e) { console.log('DidDismiss', e) })
window.addEventListener("ionModalWillDismiss", function (e) { console.log('WillDismiss', e) })
function createModal(options) {
let items = '';
for (var i = 0; i < 25; i++ ) {
items += `<ion-item>Item ${i}</ion-item>`;
}
// create component to open
const element = document.createElement('div');
element.innerHTML = `
<ion-header>
<ion-toolbar>
<ion-title>Super Modal</ion-title>
<ion-buttons slot="end">
<ion-button class="dismiss">Dismiss Modal</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-list>
${items}
</ion-list>
</ion-content>
`;
let extraOptions = {
initialBreakpoint: 0.25,
breakpoints: [0, 0.25, .5, .75, 1]
};
if (options) {
extraOptions = {
...extraOptions,
...options
}
}
// present the modal
const modalElement = Object.assign(document.createElement('ion-modal'), {
component: element,
...extraOptions
});
// listen for close event
const button = element.querySelector('ion-button');
button.addEventListener('click', () => {
modalElement.dismiss();
});
document.body.appendChild(modalElement);
return modalElement;
}
async function presentModal(options) {
const modal = createModal(options);
await modal.present();
}
async function presentCardModal() {
const presentingEl = document.querySelectorAll('.ion-page')[1];
const modal = createModal('card', {
presentingElement: presentingEl
});
await modal.present();
}
</script>
</body>
</html>

View File

@@ -128,7 +128,7 @@ import { EventModalModule } from '../modals/event/event.module';
export class CalendarComponentModule {}
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -170,6 +170,34 @@ async presentModal() {
}
```
### Sheet Modals
**Controller**
```javascript
import { IonRouterOutlet } from '@ionic/angular';
constructor(private routerOutlet: IonRouterOutlet) {}
async presentModal() {
const modal = await this.modalController.create({
component: ModalPage,
initialBreakpoint: 0.5,
breakpoints: [0, 0.5, 1]
});
return await modal.present();
}
```
**Inline**
```html
<ion-modal [isOpen]="isModalOpen" [initialBreakpoint]="0.5" [breakpoints]="[0, 0.5, 1]">
<ng-template>
<modal-page></modal-page>
</ng-template>
</ion-modal>
```
### Style Placement

View File

@@ -82,7 +82,7 @@ console.log(data);
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -105,3 +105,12 @@ modalElement.cssClass = 'my-custom-class';
modalElement.swipeToClose = true;
modalElement.presentingElement = await modalController.getTop(); // Get the top-most ion-modal
```
### Sheet Modals
```javascript
const modalElement = document.createElement('ion-modal');
modalElement.component = 'modal-page';
modalElement.initialBreakpoint = 0.5;
modalElement.breakpoints = [0, 0.5, 1];
```

View File

@@ -81,7 +81,7 @@ export const ModalExample: React.FC = () => {
};
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -112,18 +112,18 @@ const Home: React.FC<HomePageProps> = ({ router }) => {
const [showModal, setShowModal] = useState(false);
return (
...
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
...
<IonPage>
<IonContent>
<IonModal
isOpen={showModal}
cssClass='my-custom-class'
swipeToClose={true}
presentingElement={router || undefined}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
</IonContent>
</IonPage>
);
};
@@ -153,3 +153,43 @@ In most scenarios, setting a ref on `IonRouterOutlet` and passing that ref's `cu
<IonButton onClick={() => setShow2ndModal(false)}>Close Modal</IonButton>
</IonModal>
```
### Sheet Modals
```tsx
const App: React.FC = () => {
const routerRef = useRef<HTMLIonRouterOutletElement | null>(null);
return (
<IonApp>
<IonReactRouter>
<IonRouterOutlet ref={routerRef}>
<Route path="/home" render={() => <Home router={routerRef.current} />} exact={true} />
</IonRouterOutlet>
</IonReactRouter>
</IonApp>
)
};
...
const Home: React.FC = () => {
const [showModal, setShowModal] = useState(false);
return (
<IonPage>
<IonContent>
<IonModal
isOpen={showModal}
initialBreakpoint={0.5}
breakpoints={[0, 0.5, 1]}
onDidDismiss={() => setShowModal(false)}>
<p>This is modal content</p>
</IonModal>
</IonContent>
</IonPage>
);
};
```

View File

@@ -107,7 +107,7 @@ const { data } = await modal.onWillDismiss();
console.log(data);
```
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -151,3 +151,56 @@ async presentModal() {
await modal.present();
}
```
### Sheet Modals
**Controller**
```tsx
import { Component, Element, h } from '@stencil/core';
import { modalController } from '@ionic/core';
@Component({
tag: 'modal-example',
styleUrl: 'modal-example.css'
})
export class ModalExample {
@Element() el: any;
async presentModal() {
const modal = await modalController.create({
component: 'page-modal',
initialBreakpoint: 0.5,
breakpoints: [0, 0.5, 1]
});
await modal.present();
}
}
```
**Inline**
```tsx
import { Component, State, h } from '@stencil/core';
@Component({
tag: 'modal-example',
styleUrl: 'modal-example.css'
})
export class ModalExample {
@State() isModalOpen: boolean = false;
render() {
return [
<ion-modal
isOpen={isModalOpen}
initialBreakpoint={0.5}
breakpoints={[0, 0.5, 1]}
>
<page-modal></page-modal>
<ion-modal>
]
}
}
```

View File

@@ -94,7 +94,7 @@ export default defineComponent({
> If you need a wrapper element inside of your modal component, we recommend using an `<ion-page>` so that the component dimensions are still computed properly.
### Swipeable Modals
### Card Modals
Modals in iOS mode have the ability to be presented in a card-style and swiped to close. The card-style presentation and swipe to close gesture are not mutually exclusive, meaning you can pick and choose which features you want to use. For example, you can have a card-style modal that cannot be swiped or a full sized modal that can be swiped.
@@ -133,4 +133,71 @@ export default defineComponent({
}
});
</script>
```
### Sheet Modals
**Controller**
```html
<template>
<ion-page>
<ion-content class="ion-padding">
<ion-button @click="openModal()">Open Modal</ion-button>
</ion-content>
</ion-page>
</template>
<script>
import { IonButton, IonContent, IonPage, modalController } from '@ionic/vue';
import Modal from './modal.vue'
export default {
components: { IonButton, IonContent, IonPage },
methods: {
async openModal() {
const modal = await modalController
.create({
component: Modal,
initialBreakpoint: 0.5,
breakpoints: [0, 0.5, 1]
})
return modal.present();
},
},
}
</script>
```
**Inline**
```html
<template>
<ion-page>
<ion-content>
<ion-button @click="setOpen(true)">Show Modal</ion-button>
<ion-modal
:is-open="isOpenRef"
:initial-breakpoint="0.5"
:breakpoints="[0, 0.5, 1]"
@didDismiss="setOpen(false)"
>
<Modal></Modal>
</ion-modal>
</ion-content>
</ion-page>
</template>
<script lang="ts">
import { IonModal, IonButton, IonContent, IonPage } from '@ionic/vue';
import { defineComponent, ref } from 'vue';
import Modal from './modal.vue'
export default defineComponent({
components: { IonModal, IonButton, Modal, IonContent, IonPage },
setup() {
const isOpenRef = ref(false);
const setOpen = (state: boolean) => isOpenRef.value = state;
return { isOpenRef, setOpen }
}
});
</script>
```

View File

@@ -0,0 +1,48 @@
/**
* Use y = mx + b to
* figure out the backdrop value
* at a particular x coordinate. This
* is useful when the backdrop does
* not begin to fade in until after
* the 0 breakpoint.
*/
export const getBackdropValueForSheet = (x: number, maxBreakpoint: number, backdropBreakpoint: number) => {
/**
* We will use these points:
* (backdropBreakpoint, 0)
* (maxBreakpoint, 1)
* We know that at the beginning breakpoint,
* the backdrop will be hidden. We also
* know that at the maxBreakpoint, the backdrop
* must be fully visible.
* m = (y2 - y1) / (x2 - x1)
*
* This is simplified from:
* m = (1 - 0) / (maxBreakpoint - backdropBreakpoint)
*/
const slope = 1 / (maxBreakpoint - backdropBreakpoint);
/**
* From here, compute b which is
* the backdrop opacity if the offset
* is 0. If the backdrop does not
* begin to fade in until after the
* 0 breakpoint, this b value will be
* negative. This is fine as we never pass
* b directly into the animation keyframes.
* b = y - mx
* Use a known point: (backdropBreakpoint, 0)
* This is simplified from:
* b = 0 - (backdropBreakpoint * slope)
*/
const b = -(backdropBreakpoint * slope);
/**
* Finally, we can now determine the
* backdrop offset given an arbitrary
* gesture offset.
*/
return (x * slope) + b;
}

View File

@@ -159,6 +159,63 @@ const PickerExample: React.FC = () => {
```
### Vue
```vue
<template>
<div>
<ion-button @click="openPicker">SHOW PICKER</ion-button>
<p v-if="picked.animal">picked: {{ picked.animal.text }}</p>
</div>
</template>
<script>
import { IonButton, pickerController } from "@ionic/vue";
export default {
components: {
IonButton,
},
data() {
return {
pickingOptions: {
name: "animal",
options: [
{ text: "Dog", value: "dog" },
{ text: "Cat", value: "cat" },
{ text: "Bird", value: "bird" },
],
},
picked: {
animal: "",
},
};
},
methods: {
async openPicker() {
const picker = await pickerController.create({
columns: [this.pickingOptions],
buttons: [
{
text: "Cancel",
role: "cancel",
},
{
text: "Confirm",
handler: (value) => {
this.picked = value;
console.log(`Got Value ${value}`);
},
},
],
});
await picker.present();
},
},
};
</script>
```
## Properties

View File

@@ -0,0 +1,53 @@
```vue
<template>
<div>
<ion-button @click="openPicker">SHOW PICKER</ion-button>
<p v-if="picked.animal">picked: {{ picked.animal.text }}</p>
</div>
</template>
<script>
import { IonButton, pickerController } from "@ionic/vue";
export default {
components: {
IonButton,
},
data() {
return {
pickingOptions: {
name: "animal",
options: [
{ text: "Dog", value: "dog" },
{ text: "Cat", value: "cat" },
{ text: "Bird", value: "bird" },
],
},
picked: {
animal: "",
},
};
},
methods: {
async openPicker() {
const picker = await pickerController.create({
columns: [this.pickingOptions],
buttons: [
{
text: "Cancel",
role: "cancel",
},
{
text: "Confirm",
handler: (value) => {
this.picked = value;
console.log(`Got Value ${value}`);
},
},
],
});
await picker.present();
},
},
};
</script>
```

View File

@@ -1,4 +1,8 @@
import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Mode } from '../../interface';
import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Mode, OverlayInterface } from '../../interface';
export interface PopoverInterface extends OverlayInterface {
present: (event?: MouseEvent | TouchEvent | PointerEvent) => Promise<void>;
}
export interface PopoverOptions<T extends ComponentRef = ComponentRef> {
component: T;

View File

@@ -1,7 +1,7 @@
import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Method, Prop, State, Watch, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, OverlayEventDetail, OverlayInterface, PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from '../../interface';
import { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, OverlayEventDetail, PopoverInterface, PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from '../../interface';
import { CoreDelegate, attachComponent, detachComponent } from '../../utils/framework-delegate';
import { addEventListener, raf } from '../../utils/helpers';
import { BACKDROP, dismiss, eventMethod, focusFirstDescendant, prepareOverlay, present } from '../../utils/overlays';
@@ -32,7 +32,7 @@ import { configureDismissInteraction, configureKeyboardInteraction, configureTri
},
shadow: true
})
export class Popover implements ComponentInterface, OverlayInterface {
export class Popover implements ComponentInterface, PopoverInterface {
private usersElement?: HTMLElement;
private triggerEl?: HTMLElement | null;
@@ -48,7 +48,6 @@ export class Popover implements ComponentInterface, OverlayInterface {
private inline = false;
private workingDelegate?: FrameworkDelegate;
private triggerEv?: Event;
private focusDescendantOnPresent = false;
lastFocus?: HTMLElement;
@@ -305,12 +304,10 @@ export class Popover implements ComponentInterface, OverlayInterface {
*/
@Method()
async presentFromTrigger(event?: any, focusDescendant = false) {
this.triggerEv = event;
this.focusDescendantOnPresent = focusDescendant;
await this.present();
await this.present(event);
this.triggerEv = undefined;
this.focusDescendantOnPresent = false;
}
@@ -349,9 +346,12 @@ export class Popover implements ComponentInterface, OverlayInterface {
/**
* Present the popover overlay after it has been created.
* Developers can pass a mouse, touch, or pointer event
* to position the popover relative to where that event
* was dispatched.
*/
@Method()
async present(): Promise<void> {
async present(event?: MouseEvent | TouchEvent | PointerEvent): Promise<void> {
if (this.presented) {
return;
}
@@ -381,7 +381,7 @@ export class Popover implements ComponentInterface, OverlayInterface {
this.configureDismissInteraction();
this.currentTransition = present(this, 'popoverEnter', iosEnterAnimation, mdEnterAnimation, {
event: this.event || this.triggerEv,
event: event || this.event,
size: this.size,
trigger: this.triggerEl,
reference: this.reference,

View File

@@ -577,9 +577,12 @@ Type: `Promise<OverlayEventDetail<T>>`
### `present() => Promise<void>`
### `present(event?: MouseEvent | TouchEvent | PointerEvent | undefined) => Promise<void>`
Present the popover overlay after it has been created.
Developers can pass a mouse, touch, or pointer event
to position the popover relative to where that event
was dispatched.
#### Returns

View File

@@ -1,10 +1,10 @@
import { newE2EPage } from '@stencil/core/testing';
test('popover: inline', async () => {
test('popover: inline, isOpen and event props', async () => {
const page = await newE2EPage({ url: '/src/components/popover/test/inline?ionic:_testing=true' });
const screenshotCompares = [];
await page.click('ion-button');
await page.click('ion-button#props');
await page.waitForSelector('ion-popover');
let popover = await page.find('ion-popover');
@@ -21,7 +21,43 @@ test('popover: inline', async () => {
popover = await page.find('ion-popover');
await page.click('ion-button');
await page.click('ion-button#props');
await page.waitForSelector('ion-popover');
let popoverAgain = await page.find('ion-popover');
expect(popoverAgain).not.toBe(null);
await popoverAgain.waitForVisible();
screenshotCompares.push(await page.compareScreenshot());
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
test('popover: inline, present method', async () => {
const page = await newE2EPage({ url: '/src/components/popover/test/inline?ionic:_testing=true' });
const screenshotCompares = [];
await page.click('ion-button#method');
await page.waitForSelector('ion-popover');
let popover = await page.find('ion-popover');
expect(popover).not.toBe(null);
await popover.waitForVisible();
screenshotCompares.push(await page.compareScreenshot());
await popover.callMethod('dismiss');
await popover.waitForNotVisible();
screenshotCompares.push(await page.compareScreenshot('dismiss'));
popover = await page.find('ion-popover');
await page.click('ion-button#method');
await page.waitForSelector('ion-popover');
let popoverAgain = await page.find('ion-popover');

View File

@@ -18,7 +18,8 @@
</ion-header>
<ion-content class="ion-padding">
<ion-button onclick="openPopover(event)">Open Popover</ion-button>
<ion-button id="props" onclick="openPopover(event)">Open Popover via Props</ion-button>
<ion-button id="method" onclick="openPopoverMethod(event)">Open Popover via Present Method</ion-button>
<ion-popover>
<ion-content class="ion-padding">
@@ -35,6 +36,10 @@
popover.event = ev;
}
const openPopoverMethod = (ev) => {
popover.present(ev);
}
popover.addEventListener('didDismiss', () => {
popover.isOpen = false;
popover.event = undefined;

View File

@@ -245,17 +245,16 @@ export class ReorderGroup implements ComponentInterface {
private itemIndexForTop(deltaY: number): number {
const heights = this.cachedHeights;
let i = 0;
// TODO: since heights is a sorted array of integers, we can do
// speed up the search using binary search. Remember that linear-search is still
// faster than binary-search for small arrays (<64) due CPU branch misprediction.
for (i = 0; i < heights.length; i++) {
for (let i = 0; i < heights.length; i++) {
if (heights[i] > deltaY) {
break;
return i;
}
}
return i;
return heights.length - 1;
}
/********* DOM WRITE ********* */

View File

@@ -127,7 +127,7 @@
console.log('slide transition start', e)
});
slides.addEventListener('ionSlideTransitionEnd', function (e) {
console.log('slide transistion end', e)
console.log('slide transition end', e)
});
slides.addEventListener('ionSlideDrag', function (e) {
console.log('slide drag', e)

View File

@@ -40,6 +40,7 @@ body.backdrop-no-scroll {
* padding though because of the safe area.
*/
html.ios ion-modal.modal-card ion-header ion-toolbar:first-of-type,
html.ios ion-modal.modal-sheet ion-header ion-toolbar:first-of-type,
html.ios ion-modal ion-footer ion-toolbar:first-of-type {
padding-top: 6px;
}
@@ -49,7 +50,8 @@ html.ios ion-modal ion-footer ion-toolbar:first-of-type {
* bottom of the header. We accomplish this by targeting
* the last toolbar in the header.
*/
html.ios ion-modal.modal-card ion-header ion-toolbar:last-of-type {
html.ios ion-modal.modal-card ion-header ion-toolbar:last-of-type,
html.ios ion-modal.modal-sheet ion-header ion-toolbar:last-of-type {
padding-bottom: 6px;
}

View File

@@ -409,11 +409,31 @@ export const createAnimation = (animationId?: string): Animation => {
};
const keyframes = (keyframeValues: AnimationKeyFrames) => {
const different = _keyframes !== keyframeValues;
_keyframes = keyframeValues;
if (different) {
updateKeyframes(_keyframes);
}
return ani;
};
const updateKeyframes = (keyframeValues: AnimationKeyFrames) => {
if (supportsWebAnimations) {
getWebAnimations().forEach(animation => {
if (animation.effect.setKeyframes) {
animation.effect.setKeyframes(keyframeValues);
} else {
const newEffect = new KeyframeEffect(animation.effect.target, keyframeValues, animation.effect.getTiming());
animation.effect = newEffect;
}
});
} else {
initializeCSSAnimation();
}
};
/**
* Run all "before" animation hooks.
*/
@@ -668,9 +688,8 @@ export const createAnimation = (animationId?: string): Animation => {
if (!initialized) {
initializeAnimation();
} else {
update(false, true, step);
}
update(false, true, step);
return ani;
};

View File

@@ -376,7 +376,6 @@ export const present = async (
if (completed) {
overlay.didPresent.emit();
overlay.didPresentShorthand?.emit();
}
/**
@@ -528,6 +527,13 @@ export const isCancel = (role: string | undefined): boolean => {
const defaultGate = (h: any) => h();
/**
* Calls a developer provided method while avoiding
* Angular Zones. Since the handler is provided by
* the developer, we should throw any errors
* received so that developer-provided bug
* tracking software can log it.
*/
export const safeCall = (handler: any, arg?: any) => {
if (typeof handler === 'function') {
const jmp = config.get('_zoneGate', defaultGate);
@@ -535,7 +541,7 @@ export const safeCall = (handler: any, arg?: any) => {
try {
return handler(arg);
} catch (e) {
console.error(e);
throw e;
}
});
}

View File

@@ -1,6 +1,7 @@
import { Config } from '@stencil/core';
import { sass } from '@stencil/sass';
import { vueOutputTarget } from '@stencil/vue-output-target';
import { reactOutputTarget } from '@stencil/react-output-target';
// @ts-ignore
import { apiSpecGenerator } from './scripts/api-spec-generator';
@@ -61,6 +62,40 @@ export const config: Config = {
})
],
outputTargets: [
reactOutputTarget({
componentCorePackage: '@ionic/core',
includePolyfills: false,
includeDefineCustomElements: false,
proxiesFile: '../packages/react/src/components/proxies.ts',
excludeComponents: [
// Routing
'ion-router',
'ion-route',
'ion-route-redirect',
'ion-router-link',
'ion-router-outlet',
'ion-back-button',
'ion-tab-button',
'ion-tabs',
'ion-tab-bar',
'ion-button',
'ion-card',
'ion-fab-button',
'ion-item',
'ion-item-option',
// Overlays
'ion-action-sheet',
'ion-alert',
'ion-loading',
'ion-modal',
'ion-picker',
'ion-popover',
'ion-toast',
'ion-icon'
]
}),
vueOutputTarget({
componentCorePackage: '@ionic/core',
includeImportCustomElements: true,

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/docs",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "Pre-packaged API documentation for the Ionic docs.",
"main": "core.json",
"types": "core.d.ts",

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/angular-server",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/angular-server",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"license": "MIT",
"devDependencies": {
"@angular/animations": "8.2.13",
@@ -16,7 +16,7 @@
"@angular/core": "8.2.13",
"@angular/platform-browser": "8.2.13",
"@angular/platform-server": "8.2.13",
"@ionic/core": "6.0.0-beta.3",
"@ionic/core": "6.0.0-beta.4",
"ng-packagr": "5.7.1",
"tslint": "^5.12.1",
"tslint-ionic-rules": "0.0.21",
@@ -137,9 +137,9 @@
}
},
"node_modules/@ionic/core": {
"version": "6.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.3.tgz",
"integrity": "sha512-lkSjMPdNwkqJ2rJfyTEy8W9WyTTq+rpvci5ZBKXhqNCBdIhXRxBKNPDHjI8B2qbleEKAu05hJMUFMZTGFSDN8w==",
"version": "6.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.4.tgz",
"integrity": "sha512-yjw0v/NTdxUiBwyWydwOliFHHxE8t5iQy3Sl3TVLlKV9Dx6xuSRHJAiFf+p57KUeST+M8EvDwdgIffLAT9U93g==",
"dev": true,
"dependencies": {
"@stencil/core": "^2.6.0",
@@ -5424,9 +5424,9 @@
}
},
"@ionic/core": {
"version": "6.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.3.tgz",
"integrity": "sha512-lkSjMPdNwkqJ2rJfyTEy8W9WyTTq+rpvci5ZBKXhqNCBdIhXRxBKNPDHjI8B2qbleEKAu05hJMUFMZTGFSDN8w==",
"version": "6.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.4.tgz",
"integrity": "sha512-yjw0v/NTdxUiBwyWydwOliFHHxE8t5iQy3Sl3TVLlKV9Dx6xuSRHJAiFf+p57KUeST+M8EvDwdgIffLAT9U93g==",
"dev": true,
"requires": {
"@stencil/core": "^2.6.0",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/angular-server",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "Angular SSR Module for Ionic",
"keywords": [
"ionic",
@@ -49,7 +49,7 @@
"@angular/core": "8.2.13",
"@angular/platform-browser": "8.2.13",
"@angular/platform-server": "8.2.13",
"@ionic/core": "6.0.0-beta.4",
"@ionic/core": "6.0.0-beta.5",
"ng-packagr": "5.7.1",
"tslint": "^5.12.1",
"tslint-ionic-rules": "0.0.21",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react-router",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "React Router wrapper for @ionic/react",
"keywords": [
"ionic",
@@ -39,15 +39,15 @@
"tslib": "*"
},
"peerDependencies": {
"@ionic/react": "6.0.0-beta.4",
"@ionic/react": "6.0.0-beta.5",
"react": ">=16.8.6",
"react-dom": ">=16.8.6",
"react-router": "^5.0.1",
"react-router-dom": "^5.0.1"
},
"devDependencies": {
"@ionic/core": "6.0.0-beta.4",
"@ionic/react": "6.0.0-beta.4",
"@ionic/core": "6.0.0-beta.5",
"@ionic/react": "6.0.0-beta.5",
"@rollup/plugin-node-resolve": "^8.1.0",
"@testing-library/jest-dom": "^5.11.6",
"@testing-library/react": "^11.2.2",

View File

@@ -77,7 +77,7 @@
"style-loader": "0.23.1",
"terser-webpack-plugin": "2.3.4",
"ts-pnp": "1.1.5",
"typescript": "3.7.4",
"typescript": "^3.9.5",
"url-loader": "2.3.0",
"wait-on": "^5.3.0",
"webpack": "4.41.5",
@@ -19603,9 +19603,10 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"node_modules/typescript": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.4.tgz",
"integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw==",
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -37499,9 +37500,9 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c="
},
"typescript": {
"version": "3.7.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.4.tgz",
"integrity": "sha512-A25xv5XCtarLwXpcDNZzCGvW2D1S3/bACratYBx2sax8PefsFhlYmkQicKHvpYflFS8if4zne5zT5kpJ7pzuvw=="
"version": "3.9.10",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.10.tgz",
"integrity": "sha512-w6fIxVE/H1PkLKcCPsFqKE7Kv7QUwhU8qQY2MueZXWx5cPZdwFupLgKK3vntcK98BtNHZtAF4LA/yl2a7k8R6Q=="
},
"undefsafe": {
"version": "2.0.3",

View File

@@ -72,7 +72,7 @@
"style-loader": "0.23.1",
"terser-webpack-plugin": "2.3.4",
"ts-pnp": "1.1.5",
"typescript": "3.7.4",
"typescript": "^3.9.5",
"url-loader": "2.3.0",
"wait-on": "^5.3.0",
"webpack": "4.41.5",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/react",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "React specific wrapper for @ionic/core",
"keywords": [
"ionic",
@@ -39,7 +39,7 @@
"css/"
],
"dependencies": {
"@ionic/core": "6.0.0-beta.4",
"@ionic/core": "6.0.0-beta.5",
"ionicons": "^5.1.2",
"tslib": "*"
},

View File

@@ -4,8 +4,8 @@ import { NavContext } from '../contexts/NavContext';
import { IonicReactProps } from './IonicReactProps';
import { IonIconInner } from './inner-proxies';
import { deprecationWarning } from './react-component-lib/utils/dev';
import { createForwardRef, isPlatform } from './utils';
import { deprecationWarning } from './utils/dev';
interface IonIconProps {
ariaLabel?: string;

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { JSX } from '@ionic/core';
import { createReactComponent } from '../createComponent';
import { createReactComponent } from '../react-component-lib';
import { render, fireEvent, cleanup, RenderResult } from '@testing-library/react';
import { IonButton } from '../index';

View File

@@ -1,4 +1,4 @@
import * as utils from '../utils';
import * as utils from '../react-component-lib/utils';
import '@testing-library/jest-dom/extend-expect';
describe('isCoveredByReact', () => {

View File

@@ -1,7 +1,7 @@
import { OverlayEventDetail } from '@ionic/core';
import React from 'react';
import { attachProps, setRef } from './utils';
import { attachProps, setRef } from './react-component-lib/utils';
interface OverlayBase extends HTMLElement {
present: () => Promise<void>;

View File

@@ -4,10 +4,12 @@ import React from 'react';
import {
attachProps,
camelToDashCase,
createForwardRef,
dashToPascalCase,
isCoveredByReact,
mergeRefs,
} from './react-component-lib/utils';
import {
createForwardRef
} from './utils';
type InlineOverlayState = {

View File

@@ -2,7 +2,7 @@ import { OverlayEventDetail } from '@ionic/core';
import React from 'react';
import ReactDOM from 'react-dom';
import { attachProps, setRef } from './utils';
import { attachProps, setRef } from './react-component-lib/utils';
interface OverlayElement extends HTMLElement {
present: () => Promise<void>;

View File

@@ -8,10 +8,12 @@ import { RouterDirection } from '../models/RouterDirection';
import {
attachProps,
camelToDashCase,
createForwardRef,
dashToPascalCase,
isCoveredByReact,
mergeRefs,
} from './react-component-lib/utils';
import {
createForwardRef
} from './utils';
interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
@@ -24,9 +26,8 @@ interface IonicReactInternalProps<ElementType> extends React.HTMLAttributes<Elem
routerAnimation?: AnimationBuilder;
}
export const createReactComponent = <PropType, ElementType>(
tagName: string,
routerLinkComponent = false
export const createRoutingComponent = <PropType, ElementType>(
tagName: string
) => {
const displayName = dashToPascalCase(tagName);
const ReactComponent = class extends React.Component<IonicReactInternalProps<PropType>> {
@@ -86,21 +87,19 @@ export const createReactComponent = <PropType, ElementType>(
style,
};
if (routerLinkComponent) {
if (this.props.routerLink && !this.props.href) {
newProps.href = this.props.routerLink;
}
if (newProps.onClick) {
const oldClick = newProps.onClick;
newProps.onClick = (e: React.MouseEvent<PropType>) => {
oldClick(e);
if (!e.defaultPrevented) {
this.handleClick(e);
}
};
} else {
newProps.onClick = this.handleClick;
}
if (this.props.routerLink && !this.props.href) {
newProps.href = this.props.routerLink;
}
if (newProps.onClick) {
const oldClick = newProps.onClick;
newProps.onClick = (e: React.MouseEvent<PropType>) => {
oldClick(e);
if (!e.defaultPrevented) {
this.handleClick(e);
}
};
} else {
newProps.onClick = this.handleClick;
}
return React.createElement(tagName, newProps, children);

View File

@@ -34,7 +34,6 @@ export {
mdTransitionAnimation,
NavComponentWithProps,
setupConfig,
IonicSwiper,
SpinnerTypes,
@@ -63,6 +62,7 @@ export {
ToastButton
} from '@ionic/core';
export * from './proxies';
export * from './routing-proxies';
// createControllerComponent
export { IonAlert } from './IonAlert';

View File

@@ -1,7 +1,7 @@
import { JSX } from '@ionic/core';
import { JSX as IoniconsJSX } from 'ionicons';
import { /*@__PURE__*/ createReactComponent } from './createComponent';
import { /*@__PURE__*/ createReactComponent } from './react-component-lib';
export const IonTabButtonInner = /*@__PURE__*/ createReactComponent<
JSX.IonTabButton & { onIonTabButtonClick?: (e: CustomEvent) => void },

View File

@@ -1,255 +1,77 @@
import { JSX } from '@ionic/core';
/* eslint-disable */
/* tslint:disable */
/* auto-generated react proxies */
import { createReactComponent } from './react-component-lib';
import { createReactComponent } from './createComponent';
import { HrefProps } from './hrefprops';
import type { JSX } from '@ionic/core';
// ionic/core
export const IonApp = /*@__PURE__*/ createReactComponent<JSX.IonApp, HTMLIonAppElement>('ion-app');
export const IonTab = /*@__PURE__*/ createReactComponent<JSX.IonTab, HTMLIonTabElement>('ion-tab');
export const IonRouterLink = /*@__PURE__*/ createReactComponent<
HrefProps<JSX.IonRouterLink>,
HTMLIonRouterLinkElement
>('ion-router-link', true);
export const IonAccordion = /*@__PURE__*/ createReactComponent<JSX.IonAccordion, HTMLIonAccordionElement>(
'ion-accordion'
);
export const IonAccordionGroup = /*@__PURE__*/ createReactComponent<JSX.IonAccordionGroup, HTMLIonAccordionGroupElement>(
'ion-accordion-group'
);
export const IonAvatar = /*@__PURE__*/ createReactComponent<JSX.IonAvatar, HTMLIonAvatarElement>(
'ion-avatar'
);
export const IonBackdrop = /*@__PURE__*/ createReactComponent<
JSX.IonBackdrop,
HTMLIonBackdropElement
>('ion-backdrop');
export const IonBadge = /*@__PURE__*/ createReactComponent<JSX.IonBadge, HTMLIonBadgeElement>(
'ion-badge'
);
export const IonBreadcrumb = /*@__PURE__*/ createReactComponent<JSX.IonBreadcrumb, HTMLIonBreadcrumbElement>(
'ion-breadcrumb'
);
export const IonBreadcrumbs = /*@__PURE__*/ createReactComponent<JSX.IonBreadcrumbs, HTMLIonBreadcrumbsElement>(
'ion-breadcrumbs'
);
export const IonButton = /*@__PURE__*/ createReactComponent<
HrefProps<JSX.IonButton>,
HTMLIonButtonElement
>('ion-button', true);
export const IonButtons = /*@__PURE__*/ createReactComponent<JSX.IonButtons, HTMLIonButtonsElement>(
'ion-buttons'
);
export const IonCard = /*@__PURE__*/ createReactComponent<
HrefProps<JSX.IonCard>,
HTMLIonCardElement
>('ion-card', true);
export const IonCardContent = /*@__PURE__*/ createReactComponent<
JSX.IonCardContent,
HTMLIonCardContentElement
>('ion-card-content');
export const IonCardHeader = /*@__PURE__*/ createReactComponent<
JSX.IonCardHeader,
HTMLIonCardHeaderElement
>('ion-card-header');
export const IonCardSubtitle = /*@__PURE__*/ createReactComponent<
JSX.IonCardSubtitle,
HTMLIonCardSubtitleElement
>('ion-card-subtitle');
export const IonCardTitle = /*@__PURE__*/ createReactComponent<
JSX.IonCardTitle,
HTMLIonCardTitleElement
>('ion-card-title');
export const IonCheckbox = /*@__PURE__*/ createReactComponent<
JSX.IonCheckbox,
HTMLIonCheckboxElement
>('ion-checkbox');
export const IonCol = /*@__PURE__*/ createReactComponent<JSX.IonCol, HTMLIonColElement>('ion-col');
export const IonContent = /*@__PURE__*/ createReactComponent<JSX.IonContent, HTMLIonContentElement>(
'ion-content'
);
export const IonChip = /*@__PURE__*/ createReactComponent<JSX.IonChip, HTMLIonChipElement>(
'ion-chip'
);
export const IonDatetime = /*@__PURE__*/ createReactComponent<
JSX.IonDatetime,
HTMLIonDatetimeElement
>('ion-datetime');
export const IonFab = /*@__PURE__*/ createReactComponent<JSX.IonFab, HTMLIonFabElement>('ion-fab');
export const IonFabButton = /*@__PURE__*/ createReactComponent<
HrefProps<JSX.IonFabButton>,
HTMLIonFabButtonElement
>('ion-fab-button', true);
export const IonFabList = /*@__PURE__*/ createReactComponent<JSX.IonFabList, HTMLIonFabListElement>(
'ion-fab-list'
);
export const IonFooter = /*@__PURE__*/ createReactComponent<JSX.IonFooter, HTMLIonFooterElement>(
'ion-footer'
);
export const IonGrid = /*@__PURE__*/ createReactComponent<JSX.IonGrid, HTMLIonGridElement>(
'ion-grid'
);
export const IonHeader = /*@__PURE__*/ createReactComponent<JSX.IonHeader, HTMLIonHeaderElement>(
'ion-header'
);
export const IonImg = /*@__PURE__*/ createReactComponent<JSX.IonImg, HTMLIonImgElement>('ion-img');
export const IonInfiniteScroll = /*@__PURE__*/ createReactComponent<
JSX.IonInfiniteScroll,
HTMLIonInfiniteScrollElement
>('ion-infinite-scroll');
export const IonInfiniteScrollContent = /*@__PURE__*/ createReactComponent<
JSX.IonInfiniteScrollContent,
HTMLIonInfiniteScrollContentElement
>('ion-infinite-scroll-content');
export const IonInput = /*@__PURE__*/ createReactComponent<JSX.IonInput, HTMLIonInputElement>(
'ion-input'
);
export const IonItem = /*@__PURE__*/ createReactComponent<
HrefProps<JSX.IonItem>,
HTMLIonItemElement
>('ion-item', true);
export const IonItemDivider = /*@__PURE__*/ createReactComponent<
JSX.IonItemDivider,
HTMLIonItemDividerElement
>('ion-item-divider');
export const IonItemGroup = /*@__PURE__*/ createReactComponent<
JSX.IonItemGroup,
HTMLIonItemGroupElement
>('ion-item-group');
export const IonItemOption = /*@__PURE__*/ createReactComponent<
HrefProps<JSX.IonItemOption>,
HTMLIonItemOptionElement
>('ion-item-option', true);
export const IonItemOptions = /*@__PURE__*/ createReactComponent<
JSX.IonItemOptions,
HTMLIonItemOptionsElement
>('ion-item-options');
export const IonItemSliding = /*@__PURE__*/ createReactComponent<
JSX.IonItemSliding,
HTMLIonItemSlidingElement
>('ion-item-sliding');
export const IonLabel = /*@__PURE__*/ createReactComponent<JSX.IonLabel, HTMLIonLabelElement>(
'ion-label'
);
export const IonList = /*@__PURE__*/ createReactComponent<JSX.IonList, HTMLIonListElement>(
'ion-list'
);
export const IonListHeader = /*@__PURE__*/ createReactComponent<
JSX.IonListHeader,
HTMLIonListHeaderElement
>('ion-list-header');
export const IonMenu = /*@__PURE__*/ createReactComponent<JSX.IonMenu, HTMLIonMenuElement>(
'ion-menu'
);
export const IonMenuButton = /*@__PURE__*/ createReactComponent<
JSX.IonMenuButton,
HTMLIonMenuButtonElement
>('ion-menu-button');
export const IonMenuToggle = /*@__PURE__*/ createReactComponent<
JSX.IonMenuToggle,
HTMLIonMenuToggleElement
>('ion-menu-toggle');
export const IonNote = /*@__PURE__*/ createReactComponent<JSX.IonNote, HTMLIonNoteElement>(
'ion-note'
);
export const IonPickerColumn = /*@__PURE__*/ createReactComponent<
JSX.IonPickerColumn,
HTMLIonPickerColumnElement
>('ion-picker-column');
export const IonNav = /*@__PURE__*/ createReactComponent<JSX.IonNav, HTMLIonNavElement>('ion-nav');
export const IonProgressBar = /*@__PURE__*/ createReactComponent<
JSX.IonProgressBar,
HTMLIonProgressBarElement
>('ion-progress-bar');
export const IonRadio = /*@__PURE__*/ createReactComponent<JSX.IonRadio, HTMLIonRadioElement>(
'ion-radio'
);
export const IonRadioGroup = /*@__PURE__*/ createReactComponent<
JSX.IonRadioGroup,
HTMLIonRadioGroupElement
>('ion-radio-group');
export const IonRange = /*@__PURE__*/ createReactComponent<JSX.IonRange, HTMLIonRangeElement>(
'ion-range'
);
export const IonRefresher = /*@__PURE__*/ createReactComponent<
JSX.IonRefresher,
HTMLIonRefresherElement
>('ion-refresher');
export const IonRefresherContent = /*@__PURE__*/ createReactComponent<
JSX.IonRefresherContent,
HTMLIonRefresherContentElement
>('ion-refresher-content');
export const IonReorder = /*@__PURE__*/ createReactComponent<JSX.IonReorder, HTMLIonReorderElement>(
'ion-reorder'
);
export const IonReorderGroup = /*@__PURE__*/ createReactComponent<
JSX.IonReorderGroup,
HTMLIonReorderGroupElement
>('ion-reorder-group');
export const IonRippleEffect = /*@__PURE__*/ createReactComponent<
JSX.IonRippleEffect,
HTMLIonRippleEffectElement
>('ion-ripple-effect');
export const IonRow = /*@__PURE__*/ createReactComponent<JSX.IonRow, HTMLIonRowElement>('ion-row');
export const IonSearchbar = /*@__PURE__*/ createReactComponent<
JSX.IonSearchbar,
HTMLIonSearchbarElement
>('ion-searchbar');
export const IonSegment = /*@__PURE__*/ createReactComponent<JSX.IonSegment, HTMLIonSegmentElement>(
'ion-segment'
);
export const IonSegmentButton = /*@__PURE__*/ createReactComponent<
JSX.IonSegmentButton,
HTMLIonSegmentButtonElement
>('ion-segment-button');
export const IonSelect = /*@__PURE__*/ createReactComponent<JSX.IonSelect, HTMLIonSelectElement>(
'ion-select'
);
export const IonSelectOption = /*@__PURE__*/ createReactComponent<
JSX.IonSelectOption,
HTMLIonSelectOptionElement
>('ion-select-option');
export const IonSelectPopover = /*@__PURE__*/ createReactComponent<
JSX.IonSelectPopover,
HTMLIonSelectPopoverElement
>('ion-select-popover');
export const IonSkeletonText = /*@__PURE__*/ createReactComponent<
JSX.IonSkeletonText,
HTMLIonSkeletonTextElement
>('ion-skeleton-text');
export const IonSlide = /*@__PURE__*/ createReactComponent<JSX.IonSlide, HTMLIonSlideElement>(
'ion-slide'
);
export const IonSlides = /*@__PURE__*/ createReactComponent<JSX.IonSlides, HTMLIonSlidesElement>(
'ion-slides'
);
export const IonSpinner = /*@__PURE__*/ createReactComponent<JSX.IonSpinner, HTMLIonSpinnerElement>(
'ion-spinner'
);
export const IonSplitPane = /*@__PURE__*/ createReactComponent<
JSX.IonSplitPane,
HTMLIonSplitPaneElement
>('ion-split-pane');
export const IonText = /*@__PURE__*/ createReactComponent<JSX.IonText, HTMLIonTextElement>(
'ion-text'
);
export const IonTextarea = /*@__PURE__*/ createReactComponent<
JSX.IonTextarea,
HTMLIonTextareaElement
>('ion-textarea');
export const IonThumbnail = /*@__PURE__*/ createReactComponent<
JSX.IonThumbnail,
HTMLIonThumbnailElement
>('ion-thumbnail');
export const IonTitle = /*@__PURE__*/ createReactComponent<JSX.IonTitle, HTMLIonTitleElement>(
'ion-title'
);
export const IonToggle = /*@__PURE__*/ createReactComponent<JSX.IonToggle, HTMLIonToggleElement>(
'ion-toggle'
);
export const IonToolbar = /*@__PURE__*/ createReactComponent<JSX.IonToolbar, HTMLIonToolbarElement>(
'ion-toolbar'
);
export const IonVirtualScroll = /*@__PURE__*/ createReactComponent<
JSX.IonVirtualScroll,
HTMLIonVirtualScrollElement
>('ion-virtual-scroll');
export const IonAccordion = /*@__PURE__*/createReactComponent<JSX.IonAccordion, HTMLIonAccordionElement>('ion-accordion');
export const IonAccordionGroup = /*@__PURE__*/createReactComponent<JSX.IonAccordionGroup, HTMLIonAccordionGroupElement>('ion-accordion-group');
export const IonApp = /*@__PURE__*/createReactComponent<JSX.IonApp, HTMLIonAppElement>('ion-app');
export const IonAvatar = /*@__PURE__*/createReactComponent<JSX.IonAvatar, HTMLIonAvatarElement>('ion-avatar');
export const IonBackdrop = /*@__PURE__*/createReactComponent<JSX.IonBackdrop, HTMLIonBackdropElement>('ion-backdrop');
export const IonBadge = /*@__PURE__*/createReactComponent<JSX.IonBadge, HTMLIonBadgeElement>('ion-badge');
export const IonBreadcrumb = /*@__PURE__*/createReactComponent<JSX.IonBreadcrumb, HTMLIonBreadcrumbElement>('ion-breadcrumb');
export const IonBreadcrumbs = /*@__PURE__*/createReactComponent<JSX.IonBreadcrumbs, HTMLIonBreadcrumbsElement>('ion-breadcrumbs');
export const IonButtons = /*@__PURE__*/createReactComponent<JSX.IonButtons, HTMLIonButtonsElement>('ion-buttons');
export const IonCardContent = /*@__PURE__*/createReactComponent<JSX.IonCardContent, HTMLIonCardContentElement>('ion-card-content');
export const IonCardHeader = /*@__PURE__*/createReactComponent<JSX.IonCardHeader, HTMLIonCardHeaderElement>('ion-card-header');
export const IonCardSubtitle = /*@__PURE__*/createReactComponent<JSX.IonCardSubtitle, HTMLIonCardSubtitleElement>('ion-card-subtitle');
export const IonCardTitle = /*@__PURE__*/createReactComponent<JSX.IonCardTitle, HTMLIonCardTitleElement>('ion-card-title');
export const IonCheckbox = /*@__PURE__*/createReactComponent<JSX.IonCheckbox, HTMLIonCheckboxElement>('ion-checkbox');
export const IonChip = /*@__PURE__*/createReactComponent<JSX.IonChip, HTMLIonChipElement>('ion-chip');
export const IonCol = /*@__PURE__*/createReactComponent<JSX.IonCol, HTMLIonColElement>('ion-col');
export const IonContent = /*@__PURE__*/createReactComponent<JSX.IonContent, HTMLIonContentElement>('ion-content');
export const IonDatetime = /*@__PURE__*/createReactComponent<JSX.IonDatetime, HTMLIonDatetimeElement>('ion-datetime');
export const IonFab = /*@__PURE__*/createReactComponent<JSX.IonFab, HTMLIonFabElement>('ion-fab');
export const IonFabList = /*@__PURE__*/createReactComponent<JSX.IonFabList, HTMLIonFabListElement>('ion-fab-list');
export const IonFooter = /*@__PURE__*/createReactComponent<JSX.IonFooter, HTMLIonFooterElement>('ion-footer');
export const IonGrid = /*@__PURE__*/createReactComponent<JSX.IonGrid, HTMLIonGridElement>('ion-grid');
export const IonHeader = /*@__PURE__*/createReactComponent<JSX.IonHeader, HTMLIonHeaderElement>('ion-header');
export const IonImg = /*@__PURE__*/createReactComponent<JSX.IonImg, HTMLIonImgElement>('ion-img');
export const IonInfiniteScroll = /*@__PURE__*/createReactComponent<JSX.IonInfiniteScroll, HTMLIonInfiniteScrollElement>('ion-infinite-scroll');
export const IonInfiniteScrollContent = /*@__PURE__*/createReactComponent<JSX.IonInfiniteScrollContent, HTMLIonInfiniteScrollContentElement>('ion-infinite-scroll-content');
export const IonInput = /*@__PURE__*/createReactComponent<JSX.IonInput, HTMLIonInputElement>('ion-input');
export const IonItemDivider = /*@__PURE__*/createReactComponent<JSX.IonItemDivider, HTMLIonItemDividerElement>('ion-item-divider');
export const IonItemGroup = /*@__PURE__*/createReactComponent<JSX.IonItemGroup, HTMLIonItemGroupElement>('ion-item-group');
export const IonItemOptions = /*@__PURE__*/createReactComponent<JSX.IonItemOptions, HTMLIonItemOptionsElement>('ion-item-options');
export const IonItemSliding = /*@__PURE__*/createReactComponent<JSX.IonItemSliding, HTMLIonItemSlidingElement>('ion-item-sliding');
export const IonLabel = /*@__PURE__*/createReactComponent<JSX.IonLabel, HTMLIonLabelElement>('ion-label');
export const IonList = /*@__PURE__*/createReactComponent<JSX.IonList, HTMLIonListElement>('ion-list');
export const IonListHeader = /*@__PURE__*/createReactComponent<JSX.IonListHeader, HTMLIonListHeaderElement>('ion-list-header');
export const IonMenu = /*@__PURE__*/createReactComponent<JSX.IonMenu, HTMLIonMenuElement>('ion-menu');
export const IonMenuButton = /*@__PURE__*/createReactComponent<JSX.IonMenuButton, HTMLIonMenuButtonElement>('ion-menu-button');
export const IonMenuToggle = /*@__PURE__*/createReactComponent<JSX.IonMenuToggle, HTMLIonMenuToggleElement>('ion-menu-toggle');
export const IonNav = /*@__PURE__*/createReactComponent<JSX.IonNav, HTMLIonNavElement>('ion-nav');
export const IonNavLink = /*@__PURE__*/createReactComponent<JSX.IonNavLink, HTMLIonNavLinkElement>('ion-nav-link');
export const IonNote = /*@__PURE__*/createReactComponent<JSX.IonNote, HTMLIonNoteElement>('ion-note');
export const IonProgressBar = /*@__PURE__*/createReactComponent<JSX.IonProgressBar, HTMLIonProgressBarElement>('ion-progress-bar');
export const IonRadio = /*@__PURE__*/createReactComponent<JSX.IonRadio, HTMLIonRadioElement>('ion-radio');
export const IonRadioGroup = /*@__PURE__*/createReactComponent<JSX.IonRadioGroup, HTMLIonRadioGroupElement>('ion-radio-group');
export const IonRange = /*@__PURE__*/createReactComponent<JSX.IonRange, HTMLIonRangeElement>('ion-range');
export const IonRefresher = /*@__PURE__*/createReactComponent<JSX.IonRefresher, HTMLIonRefresherElement>('ion-refresher');
export const IonRefresherContent = /*@__PURE__*/createReactComponent<JSX.IonRefresherContent, HTMLIonRefresherContentElement>('ion-refresher-content');
export const IonReorder = /*@__PURE__*/createReactComponent<JSX.IonReorder, HTMLIonReorderElement>('ion-reorder');
export const IonReorderGroup = /*@__PURE__*/createReactComponent<JSX.IonReorderGroup, HTMLIonReorderGroupElement>('ion-reorder-group');
export const IonRippleEffect = /*@__PURE__*/createReactComponent<JSX.IonRippleEffect, HTMLIonRippleEffectElement>('ion-ripple-effect');
export const IonRow = /*@__PURE__*/createReactComponent<JSX.IonRow, HTMLIonRowElement>('ion-row');
export const IonSearchbar = /*@__PURE__*/createReactComponent<JSX.IonSearchbar, HTMLIonSearchbarElement>('ion-searchbar');
export const IonSegment = /*@__PURE__*/createReactComponent<JSX.IonSegment, HTMLIonSegmentElement>('ion-segment');
export const IonSegmentButton = /*@__PURE__*/createReactComponent<JSX.IonSegmentButton, HTMLIonSegmentButtonElement>('ion-segment-button');
export const IonSelect = /*@__PURE__*/createReactComponent<JSX.IonSelect, HTMLIonSelectElement>('ion-select');
export const IonSelectOption = /*@__PURE__*/createReactComponent<JSX.IonSelectOption, HTMLIonSelectOptionElement>('ion-select-option');
export const IonSkeletonText = /*@__PURE__*/createReactComponent<JSX.IonSkeletonText, HTMLIonSkeletonTextElement>('ion-skeleton-text');
export const IonSlide = /*@__PURE__*/createReactComponent<JSX.IonSlide, HTMLIonSlideElement>('ion-slide');
export const IonSlides = /*@__PURE__*/createReactComponent<JSX.IonSlides, HTMLIonSlidesElement>('ion-slides');
export const IonSpinner = /*@__PURE__*/createReactComponent<JSX.IonSpinner, HTMLIonSpinnerElement>('ion-spinner');
export const IonSplitPane = /*@__PURE__*/createReactComponent<JSX.IonSplitPane, HTMLIonSplitPaneElement>('ion-split-pane');
export const IonTab = /*@__PURE__*/createReactComponent<JSX.IonTab, HTMLIonTabElement>('ion-tab');
export const IonText = /*@__PURE__*/createReactComponent<JSX.IonText, HTMLIonTextElement>('ion-text');
export const IonTextarea = /*@__PURE__*/createReactComponent<JSX.IonTextarea, HTMLIonTextareaElement>('ion-textarea');
export const IonThumbnail = /*@__PURE__*/createReactComponent<JSX.IonThumbnail, HTMLIonThumbnailElement>('ion-thumbnail');
export const IonTitle = /*@__PURE__*/createReactComponent<JSX.IonTitle, HTMLIonTitleElement>('ion-title');
export const IonToggle = /*@__PURE__*/createReactComponent<JSX.IonToggle, HTMLIonToggleElement>('ion-toggle');
export const IonToolbar = /*@__PURE__*/createReactComponent<JSX.IonToolbar, HTMLIonToolbarElement>('ion-toolbar');
export const IonVirtualScroll = /*@__PURE__*/createReactComponent<JSX.IonVirtualScroll, HTMLIonVirtualScrollElement>('ion-virtual-scroll');

View File

@@ -0,0 +1,93 @@
import React from 'react';
import {
attachProps,
createForwardRef,
dashToPascalCase,
isCoveredByReact,
mergeRefs,
} from './utils';
export interface HTMLStencilElement extends HTMLElement {
componentOnReady(): Promise<this>;
}
interface StencilReactInternalProps<ElementType> extends React.HTMLAttributes<ElementType> {
forwardedRef: React.RefObject<ElementType>;
ref?: React.Ref<any>;
}
export const createReactComponent = <
PropType,
ElementType extends HTMLStencilElement,
ContextStateType = {},
ExpandedPropsTypes = {}
>(
tagName: string,
ReactComponentContext?: React.Context<ContextStateType>,
manipulatePropsFunction?: (
originalProps: StencilReactInternalProps<ElementType>,
propsToPass: any,
) => ExpandedPropsTypes,
) => {
const displayName = dashToPascalCase(tagName);
const ReactComponent = class extends React.Component<StencilReactInternalProps<ElementType>> {
componentEl!: ElementType;
setComponentElRef = (element: ElementType) => {
this.componentEl = element;
};
constructor(props: StencilReactInternalProps<ElementType>) {
super(props);
}
componentDidMount() {
this.componentDidUpdate(this.props);
}
componentDidUpdate(prevProps: StencilReactInternalProps<ElementType>) {
attachProps(this.componentEl, this.props, prevProps);
}
render() {
const { children, forwardedRef, style, className, ref, ...cProps } = this.props;
let propsToPass = Object.keys(cProps).reduce((acc, name) => {
if (name.indexOf('on') === 0 && name[2] === name[2].toUpperCase()) {
const eventName = name.substring(2).toLowerCase();
if (typeof document !== 'undefined' && isCoveredByReact(eventName)) {
(acc as any)[name] = (cProps as any)[name];
}
} else {
(acc as any)[name] = (cProps as any)[name];
}
return acc;
}, {});
if (manipulatePropsFunction) {
propsToPass = manipulatePropsFunction(this.props, propsToPass);
}
const newProps: Omit<StencilReactInternalProps<ElementType>, 'forwardedRef'> = {
...propsToPass,
ref: mergeRefs(forwardedRef, this.setComponentElRef),
style,
};
return React.createElement(tagName, newProps, children);
}
static get displayName() {
return displayName;
}
};
// If context was passed to createReactComponent then conditionally add it to the Component Class
if (ReactComponentContext) {
ReactComponent.contextType = ReactComponentContext;
}
return createForwardRef<PropType, ElementType>(ReactComponent, displayName);
};

View File

@@ -0,0 +1,152 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { OverlayEventDetail } from './interfaces';
import { StencilReactForwardedRef, attachProps, setRef } from './utils';
interface OverlayElement extends HTMLElement {
present: () => Promise<void>;
dismiss: (data?: any, role?: string | undefined) => Promise<boolean>;
}
export interface ReactOverlayProps {
children?: React.ReactNode;
isOpen: boolean;
onDidDismiss?: (event: CustomEvent<OverlayEventDetail>) => void;
onDidPresent?: (event: CustomEvent<OverlayEventDetail>) => void;
onWillDismiss?: (event: CustomEvent<OverlayEventDetail>) => void;
onWillPresent?: (event: CustomEvent<OverlayEventDetail>) => void;
}
export const createOverlayComponent = <
OverlayComponent extends object,
OverlayType extends OverlayElement
>(
displayName: string,
controller: { create: (options: any) => Promise<OverlayType> }
) => {
const didDismissEventName = `on${displayName}DidDismiss`;
const didPresentEventName = `on${displayName}DidPresent`;
const willDismissEventName = `on${displayName}WillDismiss`;
const willPresentEventName = `on${displayName}WillPresent`;
type Props = OverlayComponent &
ReactOverlayProps & {
forwardedRef?: StencilReactForwardedRef<OverlayType>;
};
let isDismissing = false;
class Overlay extends React.Component<Props> {
overlay?: OverlayType;
el!: HTMLDivElement;
constructor(props: Props) {
super(props);
if (typeof document !== 'undefined') {
this.el = document.createElement('div');
}
this.handleDismiss = this.handleDismiss.bind(this);
}
static get displayName() {
return displayName;
}
componentDidMount() {
if (this.props.isOpen) {
this.present();
}
}
componentWillUnmount() {
if (this.overlay) {
this.overlay.dismiss();
}
}
handleDismiss(event: CustomEvent<OverlayEventDetail<any>>) {
if (this.props.onDidDismiss) {
this.props.onDidDismiss(event);
}
setRef(this.props.forwardedRef, null)
}
shouldComponentUpdate(nextProps: Props) {
// Check if the overlay component is about to dismiss
if (this.overlay && nextProps.isOpen !== this.props.isOpen && nextProps.isOpen === false) {
isDismissing = true;
}
return true;
}
async componentDidUpdate(prevProps: Props) {
if (this.overlay) {
attachProps(this.overlay, this.props, prevProps);
}
if (prevProps.isOpen !== this.props.isOpen && this.props.isOpen === true) {
this.present(prevProps);
}
if (this.overlay && prevProps.isOpen !== this.props.isOpen && this.props.isOpen === false) {
await this.overlay.dismiss();
isDismissing = false;
/**
* Now that the overlay is dismissed
* we need to render again so that any
* inner components will be unmounted
*/
this.forceUpdate();
}
}
async present(prevProps?: Props) {
const {
children,
isOpen,
onDidDismiss,
onDidPresent,
onWillDismiss,
onWillPresent,
...cProps
} = this.props;
const elementProps = {
...cProps,
ref: this.props.forwardedRef,
[didDismissEventName]: this.handleDismiss,
[didPresentEventName]: (e: CustomEvent) =>
this.props.onDidPresent && this.props.onDidPresent(e),
[willDismissEventName]: (e: CustomEvent) =>
this.props.onWillDismiss && this.props.onWillDismiss(e),
[willPresentEventName]: (e: CustomEvent) =>
this.props.onWillPresent && this.props.onWillPresent(e),
};
this.overlay = await controller.create({
...elementProps,
component: this.el,
componentProps: {},
});
setRef(this.props.forwardedRef, this.overlay);
attachProps(this.overlay, elementProps, prevProps);
await this.overlay.present();
}
render() {
/**
* Continue to render the component even when
* overlay is dismissing otherwise component
* will be hidden before animation is done.
*/
return ReactDOM.createPortal(this.props.isOpen || isDismissing ? this.props.children : null, this.el);
}
}
return React.forwardRef<OverlayType, Props>((props, ref) => {
return <Overlay {...props} forwardedRef={ref} />;
});
};

View File

@@ -0,0 +1,2 @@
export { createReactComponent } from './createComponent';
export { createOverlayComponent } from './createOverlayComponent';

View File

@@ -0,0 +1,34 @@
// General types important to applications using stencil built components
export interface EventEmitter<T = any> {
emit: (data?: T) => CustomEvent<T>;
}
export interface StyleReactProps {
class?: string;
className?: string;
style?: { [key: string]: any };
}
export interface OverlayEventDetail<T = any> {
data?: T;
role?: string;
}
export interface OverlayInterface {
el: HTMLElement;
animated: boolean;
keyboardClose: boolean;
overlayIndex: number;
presented: boolean;
enterAnimation?: any;
leaveAnimation?: any;
didPresent: EventEmitter<void>;
willPresent: EventEmitter<void>;
willDismiss: EventEmitter<OverlayEventDetail>;
didDismiss: EventEmitter<OverlayEventDetail>;
present(): Promise<void>;
dismiss(data?: any, role?: string): Promise<boolean>;
}

View File

@@ -28,11 +28,10 @@ export const attachProps = (node: HTMLElement, newProps: any, oldProps: any = {}
syncEvent(node, eventNameLc, newProps[name]);
}
} else {
(node as any)[name] = newProps[name];
const propType = typeof newProps[name];
if (propType === 'string') {
node.setAttribute(camelToDashCase(name), newProps[name]);
} else {
(node as any)[name] = newProps[name];
}
}
});

View File

@@ -0,0 +1,47 @@
import React from 'react';
import type { StyleReactProps } from '../interfaces';
export type StencilReactExternalProps<PropType, ElementType> = PropType &
Omit<React.HTMLAttributes<ElementType>, 'style'> &
StyleReactProps;
// This will be replaced with React.ForwardedRef when react-output-target is upgraded to React v17
export type StencilReactForwardedRef<T> = ((instance: T | null) => void) | React.MutableRefObject<T | null> | null;
export const setRef = (ref: StencilReactForwardedRef<any> | React.Ref<any> | undefined, value: any) => {
if (typeof ref === 'function') {
ref(value)
} else if (ref != null) {
// Cast as a MutableRef so we can assign current
(ref as React.MutableRefObject<any>).current = value
}
};
export const mergeRefs = (
...refs: (StencilReactForwardedRef<any> | React.Ref<any> | undefined)[]
): React.RefCallback<any> => {
return (value: any) => {
refs.forEach(ref => {
setRef(ref, value)
})
}
};
export const createForwardRef = <PropType, ElementType>(
ReactComponent: any,
displayName: string,
) => {
const forwardRef = (
props: StencilReactExternalProps<PropType, ElementType>,
ref: StencilReactForwardedRef<ElementType>,
) => {
return <ReactComponent {...props} forwardedRef={ref} />;
};
forwardRef.displayName = displayName;
return React.forwardRef(forwardRef);
};
export * from './attachProps';
export * from './case';

View File

@@ -0,0 +1,34 @@
import type { JSX } from '@ionic/core';
import { createRoutingComponent } from './createRoutingComponent';
import { HrefProps } from './hrefprops';
export const IonRouterLink = /*@__PURE__*/ createRoutingComponent<
HrefProps<JSX.IonRouterLink>,
HTMLIonRouterLinkElement
>('ion-router-link');
export const IonButton = /*@__PURE__*/ createRoutingComponent<
HrefProps<JSX.IonButton>,
HTMLIonButtonElement
>('ion-button');
export const IonCard = /*@__PURE__*/ createRoutingComponent<
HrefProps<JSX.IonCard>,
HTMLIonCardElement
>('ion-card');
export const IonFabButton = /*@__PURE__*/ createRoutingComponent<
HrefProps<JSX.IonFabButton>,
HTMLIonFabButtonElement
>('ion-fab-button');
export const IonItem = /*@__PURE__*/ createRoutingComponent<
HrefProps<JSX.IonItem>,
HTMLIonItemElement
>('ion-item');
export const IonItemOption = /*@__PURE__*/ createRoutingComponent<
HrefProps<JSX.IonItemOption>,
HTMLIonItemOptionElement
>('ion-item-option');

View File

@@ -27,28 +27,6 @@ export const createForwardRef = <PropType, ElementType>(
return React.forwardRef(forwardRef);
};
export const setRef = (ref: React.ForwardedRef<any> | React.Ref<any> | undefined, value: any) => {
if (typeof ref === 'function') {
ref(value)
} else if (ref != null) {
// Cast as a MutableRef so we can assign current
(ref as React.MutableRefObject<any>).current = value
}
};
export const mergeRefs = (
...refs: (React.ForwardedRef<any> | React.Ref<any> | undefined)[]
): React.RefCallback<any> => {
return (value: any) => {
refs.forEach(ref => {
setRef(ref, value)
})
}
};
export * from './attachProps';
export * from './case';
export const isPlatform = (platform: Platforms) => {
return isPlatformCore(window, platform);
};

View File

@@ -1,7 +1,7 @@
import { OverlayEventDetail } from '@ionic/core';
import { useMemo, useRef } from 'react';
import { attachProps } from '../components/utils';
import { attachProps } from '../components/react-component-lib/utils';
import { HookOverlayOptions } from './HookOverlayOptions';

View File

@@ -2,7 +2,7 @@ import { OverlayEventDetail } from '@ionic/core';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import { attachProps } from '../components/utils';
import { attachProps } from '../components/react-component-lib/utils';
import { HookOverlayOptions } from './HookOverlayOptions';

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { mergeRefs } from '../components/utils';
import { mergeRefs } from '../components/react-component-lib/utils';
import { IonLifeCycleContext } from '../contexts/IonLifeCycleContext';
import { RouteInfo } from '../models';

View File

@@ -1,12 +1,12 @@
{
"name": "@ionic/vue-router",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue-router",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"license": "MIT",
"devDependencies": {
"@ionic/vue": "5.4.1",
@@ -23,7 +23,7 @@
},
"../../core": {
"name": "@ionic/core",
"version": "6.0.0-beta.3",
"version": "6.0.0-beta.4",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -36,6 +36,7 @@
"@jest/core": "^26.6.3",
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/react-output-target": "^0.0.12",
"@stencil/sass": "1.3.2",
"@stencil/vue-output-target": "^0.5.1",
"@types/jest": "^26.0.20",
@@ -7289,6 +7290,7 @@
"@rollup/plugin-node-resolve": "^8.4.0",
"@rollup/plugin-virtual": "^2.0.3",
"@stencil/core": "^2.6.0",
"@stencil/react-output-target": "^0.0.12",
"@stencil/sass": "1.3.2",
"@stencil/vue-output-target": "^0.5.1",
"@types/jest": "^26.0.20",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue-router",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "Vue Router integration for @ionic/vue",
"scripts": {
"test.spec": "jest",

View File

@@ -20,6 +20,14 @@ export const createViewStacks = (router: Router) => {
const registerIonPage = (viewItem: ViewItem, ionPage: HTMLElement) => {
viewItem.ionPageElement = ionPage;
viewItem.ionRoute = true;
/**
* This is needed otherwise Vue Router
* will not consider this component mounted
* and will not run route guards that
* are written in the component.
*/
viewItem.matchedRoute.instances = { default: viewItem.vueComponentRef.value };
}
const findViewItemByRouteInfo = (routeInfo: RouteInfo, outletId?: number) => {
@@ -186,6 +194,7 @@ export const createViewStacks = (router: Router) => {
viewItem.mount = false;
viewItem.ionPageElement = undefined;
viewItem.ionRoute = false;
viewItem.matchedRoute.instances = {};
}
}

View File

@@ -1,15 +1,15 @@
{
"name": "@ionic/vue",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "@ionic/vue",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"license": "MIT",
"dependencies": {
"@ionic/core": "6.0.0-beta.3",
"@ionic/core": "6.0.0-beta.4",
"ionicons": "^5.1.2"
},
"devDependencies": {
@@ -53,9 +53,9 @@
}
},
"node_modules/@ionic/core": {
"version": "6.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.3.tgz",
"integrity": "sha512-lkSjMPdNwkqJ2rJfyTEy8W9WyTTq+rpvci5ZBKXhqNCBdIhXRxBKNPDHjI8B2qbleEKAu05hJMUFMZTGFSDN8w==",
"version": "6.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.4.tgz",
"integrity": "sha512-yjw0v/NTdxUiBwyWydwOliFHHxE8t5iQy3Sl3TVLlKV9Dx6xuSRHJAiFf+p57KUeST+M8EvDwdgIffLAT9U93g==",
"dependencies": {
"@stencil/core": "^2.6.0",
"ionicons": "^5.5.1",
@@ -633,9 +633,9 @@
}
},
"@ionic/core": {
"version": "6.0.0-beta.3",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.3.tgz",
"integrity": "sha512-lkSjMPdNwkqJ2rJfyTEy8W9WyTTq+rpvci5ZBKXhqNCBdIhXRxBKNPDHjI8B2qbleEKAu05hJMUFMZTGFSDN8w==",
"version": "6.0.0-beta.4",
"resolved": "https://registry.npmjs.org/@ionic/core/-/core-6.0.0-beta.4.tgz",
"integrity": "sha512-yjw0v/NTdxUiBwyWydwOliFHHxE8t5iQy3Sl3TVLlKV9Dx6xuSRHJAiFf+p57KUeST+M8EvDwdgIffLAT9U93g==",
"requires": {
"@stencil/core": "^2.6.0",
"ionicons": "^5.5.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@ionic/vue",
"version": "6.0.0-beta.4",
"version": "6.0.0-beta.5",
"description": "Vue specific wrapper for @ionic/core",
"scripts": {
"lint": "echo add linter",
@@ -57,7 +57,7 @@
"vue-router": "^4.0.0-rc.4"
},
"dependencies": {
"@ionic/core": "6.0.0-beta.4",
"@ionic/core": "6.0.0-beta.5",
"ionicons": "^5.1.2"
},
"vetur": {

View File

@@ -51,7 +51,7 @@ export const IonRouterOutlet = /*@__PURE__*/ defineComponent({
* page/1 to page/2 would not cause this callback
* to fire since the matchedRouteRef was the same.
*/
watch([route, matchedRouteRef], ([currentRoute, currentMatchedRouteRef], [_, previousMatchedRouteRef]) => {
watch(() => [route, matchedRouteRef.value], ([currentRoute, currentMatchedRouteRef], [_, previousMatchedRouteRef]) => {
/**
* If the matched route ref has changed,
* then we need to set up a new view item.

View File

@@ -29,7 +29,7 @@ export const IonPicker = /*@__PURE__*/ defineOverlayContainer<JSX.IonPicker>('io
export const IonToast = /*@__PURE__*/ defineOverlayContainer<JSX.IonToast>('ion-toast', IonToastCmp, ['animated', 'buttons', 'color', 'cssClass', 'duration', 'enterAnimation', 'header', 'icon', 'keyboardClose', 'leaveAnimation', 'message', 'mode', 'position', 'translucent'], toastController);
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', IonModalCmp, ['animated', 'backdropDismiss', 'enterAnimation', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'swipeToClose', 'trigger']);
export const IonModal = /*@__PURE__*/ defineOverlayContainer<JSX.IonModal>('ion-modal', IonModalCmp, ['animated', 'backdropBreakpoint', 'backdropDismiss', 'breakpoints', 'enterAnimation', 'handle', 'initialBreakpoint', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'presentingElement', 'showBackdrop', 'swipeToClose', 'trigger']);
export const IonPopover = /*@__PURE__*/ defineOverlayContainer<JSX.IonPopover>('ion-popover', IonPopoverCmp, ['alignment', 'animated', 'arrow', 'backdropDismiss', 'component', 'componentProps', 'dismissOnSelect', 'enterAnimation', 'event', 'isOpen', 'keyboardClose', 'leaveAnimation', 'mode', 'reference', 'showBackdrop', 'side', 'size', 'translucent', 'trigger', 'triggerAction']);

View File

@@ -402,4 +402,52 @@ describe('Routing', () => {
expect(pageAgain[0].props()).toEqual({ id: '1' });
expect(pageAgain[1].props()).toEqual({ id: '2' });
});
it('should fire guard written in a component', async () => {
const beforeRouteEnterSpy = jest.fn();
const beforeRouteLeaveSpy = jest.fn();
const Page = {
beforeRouteEnter() {
beforeRouteEnterSpy();
},
beforeRouteLeave() {
beforeRouteLeaveSpy();
},
components: { IonPage },
template: `<ion-page></ion-page>`
}
const Page2 = {
components: { IonPage },
template: `<ion-page></ion-page>`
}
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes: [
{ path: '/page', component: Page },
{ path: '/page2', component: Page2 },
{ path: '/', redirect: '/page' }
]
});
router.push('/');
await router.isReady();
const wrapper = mount(App, {
global: {
plugins: [router, IonicVue]
}
});
expect(beforeRouteEnterSpy).toHaveBeenCalledTimes(1);
router.push('/page2');
await waitForRouter();
expect(beforeRouteLeaveSpy).toHaveBeenCalledTimes(1);
router.back();
await waitForRouter();
expect(beforeRouteEnterSpy).toHaveBeenCalledTimes(2);
});
});