mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2026-03-13 10:22:08 +08:00
Compare commits
24 Commits
v6.0.0-bet
...
v6.0.0-bet
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
51dae5a4d6 | ||
|
|
00bae431d4 | ||
|
|
7a50992cb0 | ||
|
|
c75951354b | ||
|
|
b211cf0236 | ||
|
|
e512fc1ecd | ||
|
|
a12d146c3e | ||
|
|
a0229bc7b2 | ||
|
|
11fda41420 | ||
|
|
1680b0ce9f | ||
|
|
5ca2ce9197 | ||
|
|
12216d378d | ||
|
|
c925274c3b | ||
|
|
33f0bcd437 | ||
|
|
a212eb5259 | ||
|
|
1d2ee92ca0 | ||
|
|
3c442228ff | ||
|
|
950350a948 | ||
|
|
50b88b24c2 | ||
|
|
9317f6eb41 | ||
|
|
9932e26a2e | ||
|
|
02409f2abf | ||
|
|
864212b0f2 | ||
|
|
3d1ae0305d |
41
CHANGELOG.md
41
CHANGELOG.md
@@ -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
|
||||
|
||||
18
angular/package-lock.json
generated
18
angular/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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
20
core/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
36
core/src/components.d.ts
vendored
36
core/src/components.d.ts
vendored
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -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};
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
})}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
// --------------------------------------------------
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
10
core/src/components/item/test/form/e2e.ts
Normal file
10
core/src/components/item/test/form/e2e.ts
Normal 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();
|
||||
});
|
||||
302
core/src/components/item/test/form/index.html
Normal file
302
core/src/components/item/test/form/index.html
Normal 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>
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]);
|
||||
|
||||
59
core/src/components/modal/animations/sheet.ts
Normal file
59
core/src/components/modal/animations/sheet.ts
Normal 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 };
|
||||
}
|
||||
208
core/src/components/modal/gestures/sheet.ts
Normal file
208
core/src/components/modal/gestures/sheet.ts
Normal 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;
|
||||
};
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
45
core/src/components/modal/test/sheet/e2e.ts
Normal file
45
core/src/components/modal/test/sheet/e2e.ts
Normal 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();
|
||||
}
|
||||
});
|
||||
181
core/src/components/modal/test/sheet/index.html
Normal file
181
core/src/components/modal/test/sheet/index.html
Normal 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>
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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];
|
||||
```
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
48
core/src/components/modal/utils.ts
Normal file
48
core/src/components/modal/utils.ts
Normal 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;
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
53
core/src/components/picker/usage/vue.md
Normal file
53
core/src/components/picker/usage/vue.md
Normal 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>
|
||||
```
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 ********* */
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
18
packages/angular-server/package-lock.json
generated
18
packages/angular-server/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
15
packages/react-router/test-app/package-lock.json
generated
15
packages/react-router/test-app/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "*"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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} />;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export { createReactComponent } from './createComponent';
|
||||
export { createOverlayComponent } from './createOverlayComponent';
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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';
|
||||
34
packages/react/src/components/routing-proxies.ts
Normal file
34
packages/react/src/components/routing-proxies.ts
Normal 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');
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
8
packages/vue-router/package-lock.json
generated
8
packages/vue-router/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
18
packages/vue/package-lock.json
generated
18
packages/vue/package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user