diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index d7e064089f..90665cd466 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -4,7 +4,7 @@ title: 'bug: ' body: - type: checkboxes attributes: - label: Prequisites + label: Prerequisites description: Please ensure you have completed all of the following. options: - label: I have read the [Contributing Guidelines](https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#creating-an-issue). diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 2a955b4446..d774ff2776 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -4,7 +4,7 @@ title: 'feat: ' body: - type: checkboxes attributes: - label: Prequisites + label: Prerequisites description: Please ensure you have completed all of the following. options: - label: I have read the [Contributing Guidelines](https://github.com/ionic-team/ionic-framework/blob/main/.github/CONTRIBUTING.md#creating-an-issue). diff --git a/CHANGELOG.md b/CHANGELOG.md index 906b729732..141d25372f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,28 @@ +## [5.8.2](https://github.com/ionic-team/ionic/compare/v5.8.1...v5.8.2) (2021-10-06) + + +### Bug Fixes + +* **alert:** made it easier to tell if alert is scrollable in MD mode ([#23976](https://github.com/ionic-team/ionic/issues/23976)) ([a262753](https://github.com/ionic-team/ionic/commit/a26275378f10835343ad8a6cdea50303e6c10a14)) +* **angular:** use initialize function when setting up ionic angular to avoid config errors ([#24004](https://github.com/ionic-team/ionic/issues/24004)) ([f112ad4](https://github.com/ionic-team/ionic/commit/f112ad4490dc4a179dc3feab495530e14e655e5a)), closes [#22853](https://github.com/ionic-team/ionic/issues/22853) +* **item-sliding:** closing an item can no longer be interrupted ([#23973](https://github.com/ionic-team/ionic/issues/23973)) ([3ca0419](https://github.com/ionic-team/ionic/commit/3ca04197a4186c85d04cdf04fa9cb2689ca1bbfb)), closes [#23969](https://github.com/ionic-team/ionic/issues/23969) +* **react:** overlay hooks memorised properly to prevent re-renders ([#24010](https://github.com/ionic-team/ionic/issues/24010)) ([2c97712](https://github.com/ionic-team/ionic/commit/2c977126012ae0231d4e4fa63cc76a528bde699b)), closes [#23741](https://github.com/ionic-team/ionic/issues/23741) +* **select-popover:** non-scrollable popovers no longer have forced overscroll ([#23972](https://github.com/ionic-team/ionic/issues/23972)) ([aa4ba89](https://github.com/ionic-team/ionic/commit/aa4ba890e9c18e8a911c5188b3e2e85433658be9)), closes [#23971](https://github.com/ionic-team/ionic/issues/23971) +* **status-bar:** tapping status bar correctly scrolls content to top ([#24001](https://github.com/ionic-team/ionic/issues/24001)) ([25eb8cd](https://github.com/ionic-team/ionic/commit/25eb8cdf98fe455433ca6185e89d9e1223a6d3ae)), closes [#20423](https://github.com/ionic-team/ionic/issues/20423) + + + +## [5.8.1](https://github.com/ionic-team/ionic/compare/v5.8.0...v5.8.1) (2021-09-22) + + +### Bug Fixes + +* **angular:** select method now has correct types ([#23953](https://github.com/ionic-team/ionic/issues/23953)) ([3c1be89](https://github.com/ionic-team/ionic/commit/3c1be89112d464e77d65c875223138aaedf350cd)), closes [#23952](https://github.com/ionic-team/ionic/issues/23952) +* **item-sliding:** item-sliding accounts for multiple ion-item elements ([#23943](https://github.com/ionic-team/ionic/issues/23943)) ([8108edd](https://github.com/ionic-team/ionic/commit/8108edd876b10834015016385dc3cd5b8f31fbfa)), closes [#19312](https://github.com/ionic-team/ionic/issues/19312) +* **label:** only inherit color if color property is set on ion-item ([#23944](https://github.com/ionic-team/ionic/issues/23944)) ([ae1325c](https://github.com/ionic-team/ionic/commit/ae1325cee698066a71aae4e7deb953c4185c0926)), closes [#20125](https://github.com/ionic-team/ionic/issues/20125) + + + # [6.0.0-beta.6](https://github.com/ionic-team/ionic/compare/v6.0.0-beta.5...v6.0.0-beta.6) (2021-09-15) diff --git a/angular/src/app-initialize.ts b/angular/src/app-initialize.ts index 610d608e03..a280bc6b17 100644 --- a/angular/src/app-initialize.ts +++ b/angular/src/app-initialize.ts @@ -1,4 +1,5 @@ import { NgZone } from '@angular/core'; +import { initialize } from '@ionic/core'; import { applyPolyfills, defineCustomElements } from '@ionic/core/loader'; import { Config } from './providers/config'; @@ -9,12 +10,11 @@ export const appInitialize = (config: Config, doc: Document, zone: NgZone) => { return (): any => { const win: IonicWindow | undefined = doc.defaultView as any; if (win && typeof (window as any) !== 'undefined') { - const Ionic = win.Ionic = win.Ionic || {}; - Ionic.config = { + initialize({ ...config, _zoneGate: (h: any) => zone.run(h) - }; + }); const aelFn = '__zone_symbol__addEventListener' in (doc.body as any) ? '__zone_symbol__addEventListener' diff --git a/angular/src/directives/navigation/ion-tabs.ts b/angular/src/directives/navigation/ion-tabs.ts index af4dc7a6a4..6596980246 100644 --- a/angular/src/directives/navigation/ion-tabs.ts +++ b/angular/src/directives/navigation/ion-tabs.ts @@ -87,8 +87,9 @@ export class IonTabs { * to the default tabRootUrl */ @HostListener('ionTabButtonClick', ['$event']) - select(ev: CustomEvent) { - const tab = ev.detail.tab; + select(tabOrEvent: string | CustomEvent) { + const isTabString = typeof tabOrEvent === 'string'; + const tab = (isTabString) ? tabOrEvent : (tabOrEvent as CustomEvent).detail.tab; const alreadySelected = this.outlet.getActiveStackId() === tab; const tabRootUrl = `${this.outlet.tabsPrefix}/${tab}`; @@ -98,7 +99,9 @@ export class IonTabs { * will respond to this event too, causing * the app to get directed to the wrong place. */ - ev.stopPropagation(); + if (!isTabString) { + (tabOrEvent as CustomEvent).stopPropagation(); + } if (alreadySelected) { const activeStackId = this.outlet.getActiveStackId(); diff --git a/core/README.md b/core/README.md index b97610354d..abd975f830 100644 --- a/core/README.md +++ b/core/README.md @@ -60,7 +60,7 @@ Notice how `IonBadge` is imported from `@ionic/core/components/ion-badge` rather ## How to contribute -[Check out the CONTRIBUTE guide](CONTRIBUTING.md) +[Check out the CONTRIBUTE guide](/.github/CONTRIBUTING.md) ## Related diff --git a/core/src/components.d.ts b/core/src/components.d.ts index 7932a1dc35..b7ad27a934 100644 --- a/core/src/components.d.ts +++ b/core/src/components.d.ts @@ -2105,7 +2105,7 @@ export namespace Components { */ "pullMin": number; /** - * Time it takes the refresher to to snap back to the `refreshing` state. Does not apply when the refresher content uses a spinner, enabling the native refresher. + * Time it takes the refresher to snap back to the `refreshing` state. Does not apply when the refresher content uses a spinner, enabling the native refresher. */ "snapbackDuration": string; } @@ -2199,7 +2199,7 @@ export namespace Components { */ "push": (url: string, direction?: RouterDirection, animation?: AnimationBuilder | undefined) => Promise; /** - * By default `ion-router` will match the routes at the root path ("/"). That can be changed when + * The root path to use when matching URLs. By default, this is set to "/", but you can specify an alternate prefix for all URL paths. */ "root": string; /** @@ -5777,7 +5777,7 @@ declare namespace LocalJSX { */ "pullMin"?: number; /** - * Time it takes the refresher to to snap back to the `refreshing` state. Does not apply when the refresher content uses a spinner, enabling the native refresher. + * Time it takes the refresher to snap back to the `refreshing` state. Does not apply when the refresher content uses a spinner, enabling the native refresher. */ "snapbackDuration"?: string; } @@ -5867,7 +5867,7 @@ declare namespace LocalJSX { */ "onIonRouteWillChange"?: (event: CustomEvent) => void; /** - * By default `ion-router` will match the routes at the root path ("/"). That can be changed when + * The root path to use when matching URLs. By default, this is set to "/", but you can specify an alternate prefix for all URL paths. */ "root"?: string; /** diff --git a/core/src/components/alert/alert.md.vars.scss b/core/src/components/alert/alert.md.vars.scss index 75cc4fcee3..928132fe17 100644 --- a/core/src/components/alert/alert.md.vars.scss +++ b/core/src/components/alert/alert.md.vars.scss @@ -80,7 +80,7 @@ $alert-md-message-empty-padding-bottom: $alert-md-message-empty-padding-to $alert-md-message-empty-padding-start: $alert-md-message-empty-padding-end !default; /// @prop - Maximum height of the alert content -$alert-md-content-max-height: 240px !default; +$alert-md-content-max-height: 266px !default; /// @prop - Border width of the alert input $alert-md-input-border-width: 1px !default; diff --git a/core/src/components/item-sliding/item-sliding.tsx b/core/src/components/item-sliding/item-sliding.tsx index cd6b67841c..311aa13953 100644 --- a/core/src/components/item-sliding/item-sliding.tsx +++ b/core/src/components/item-sliding/item-sliding.tsx @@ -270,6 +270,13 @@ export class ItemSliding implements ComponentInterface { } private onStart() { + /** + * We need to query for the ion-item + * every time the gesture starts. Developers + * may toggle ion-item elements via *ngIf. + */ + this.item = this.el.querySelector('ion-item'); + // Prevent scrolling during gesture this.disableContentScrollY(); @@ -387,16 +394,28 @@ export class ItemSliding implements ComponentInterface { ? SlidingState.Start | SlidingState.SwipeStart : SlidingState.Start; } else { + /** + * Item sliding cannot be interrupted + * while closing the item. If it did, + * it would allow the item to get into an + * inconsistent state where multiple + * items are then open at the same time. + */ + if (this.gesture) { + this.gesture.enable(false); + } this.tmr = setTimeout(() => { this.state = SlidingState.Disabled; this.tmr = undefined; + if (this.gesture) { + this.gesture.enable(true); + } }, 600) as any; openSlidingItem = undefined; style.transform = ''; return; } - style.transform = `translate3d(${-openAmount}px,0,0)`; this.ionDrag.emit({ amount: openAmount, diff --git a/core/src/components/item-sliding/test/basic/index.html b/core/src/components/item-sliding/test/basic/index.html index c5ebbbc100..4a4d6f2784 100644 --- a/core/src/components/item-sliding/test/basic/index.html +++ b/core/src/components/item-sliding/test/basic/index.html @@ -41,6 +41,7 @@ Open Item Start Open Item End Open Item with only one side + Swap dynamic item @@ -369,6 +370,17 @@ + + + Dynamic First Item + + + + First Item Options + + + +

Normal ion-item (no sliding)

@@ -387,6 +399,20 @@
+ + + + + + + + + Label - Color + + + + + + Label Text

This paragraph should not inherit the color from content

+
+
+
+ + diff --git a/core/src/components/refresher/readme.md b/core/src/components/refresher/readme.md index b1af5c76b3..65bdcd972d 100644 --- a/core/src/components/refresher/readme.md +++ b/core/src/components/refresher/readme.md @@ -299,7 +299,7 @@ export default defineComponent({ | `pullFactor` | `pull-factor` | How much to multiply the pull speed by. To slow the pull animation down, pass a number less than `1`. To speed up the pull, pass a number greater than `1`. The default value is `1` which is equal to the speed of the cursor. If a negative value is passed in, the factor will be `1` instead. For example: If the value passed is `1.2` and the content is dragged by `10` pixels, instead of `10` pixels the content will be pulled by `12` pixels (an increase of 20 percent). If the value passed is `0.8`, the dragged amount will be `8` pixels, less than the amount the cursor has moved. Does not apply when the refresher content uses a spinner, enabling the native refresher. | `number` | `1` | | `pullMax` | `pull-max` | The maximum distance of the pull until the refresher will automatically go into the `refreshing` state. Defaults to the result of `pullMin + 60`. Does not apply when the refresher content uses a spinner, enabling the native refresher. | `number` | `this.pullMin + 60` | | `pullMin` | `pull-min` | The minimum distance the user must pull down until the refresher will go into the `refreshing` state. Does not apply when the refresher content uses a spinner, enabling the native refresher. | `number` | `60` | -| `snapbackDuration` | `snapback-duration` | Time it takes the refresher to to snap back to the `refreshing` state. Does not apply when the refresher content uses a spinner, enabling the native refresher. | `string` | `'280ms'` | +| `snapbackDuration` | `snapback-duration` | Time it takes the refresher to snap back to the `refreshing` state. Does not apply when the refresher content uses a spinner, enabling the native refresher. | `string` | `'280ms'` | ## Events diff --git a/core/src/components/refresher/refresher.tsx b/core/src/components/refresher/refresher.tsx index 7a9385f3fa..d31eabf929 100644 --- a/core/src/components/refresher/refresher.tsx +++ b/core/src/components/refresher/refresher.tsx @@ -82,7 +82,7 @@ export class Refresher implements ComponentInterface { @Prop() closeDuration = '280ms'; /** - * Time it takes the refresher to to snap back to the `refreshing` state. + * Time it takes the refresher to snap back to the `refreshing` state. * Does not apply when the refresher content uses a spinner, * enabling the native refresher. */ diff --git a/core/src/components/router/readme.md b/core/src/components/router/readme.md index a36e843035..3da084df83 100644 --- a/core/src/components/router/readme.md +++ b/core/src/components/router/readme.md @@ -76,7 +76,7 @@ interface RouterCustomEvent extends CustomEvent { | Property | Attribute | Description | Type | Default | | --------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | ------- | -| `root` | `root` | By default `ion-router` will match the routes at the root path ("/"). That can be changed when | `string` | `'/'` | +| `root` | `root` | The root path to use when matching URLs. By default, this is set to "/", but you can specify an alternate prefix for all URL paths. | `string` | `'/'` | | `useHash` | `use-hash` | The router can work in two "modes": - With hash: `/index.html#/path/to/page` - Without hash: `/path/to/page` Using one or another might depend in the requirements of your app and/or where it's deployed. Usually "hash-less" navigation works better for SEO and it's more user friendly too, but it might requires additional server-side configuration in order to properly work. On the other side hash-navigation is much easier to deploy, it even works over the file protocol. By default, this property is `true`, change to `false` to allow hash-less URLs. | `boolean` | `true` | diff --git a/core/src/components/router/router.tsx b/core/src/components/router/router.tsx index bcb29e3fe4..5b15779ea7 100644 --- a/core/src/components/router/router.tsx +++ b/core/src/components/router/router.tsx @@ -25,9 +25,8 @@ export class Router implements ComponentInterface { @Element() el!: HTMLElement; /** - * By default `ion-router` will match the routes at the root path ("/"). - * That can be changed when - * + * The root path to use when matching URLs. By default, this is set to "/", but you can specify + * an alternate prefix for all URL paths. */ @Prop() root = '/'; diff --git a/core/src/components/select-popover/select-popover.scss b/core/src/components/select-popover/select-popover.scss index 0c4d9782b4..3fba7de13c 100644 --- a/core/src/components/select-popover/select-popover.scss +++ b/core/src/components/select-popover/select-popover.scss @@ -1,7 +1,7 @@ -@import "./select-popover.vars"; +@import "../../themes/ionic.globals"; -ion-list { - @include margin($select-popover-list-margin-top, $select-popover-list-margin-end, $select-popover-list-margin-bottom, $select-popover-list-margin-start); +:host ion-list { + @include margin(0); } ion-list-header, diff --git a/core/src/components/select-popover/select-popover.vars.scss b/core/src/components/select-popover/select-popover.vars.scss deleted file mode 100644 index 09d0d564dd..0000000000 --- a/core/src/components/select-popover/select-popover.vars.scss +++ /dev/null @@ -1,16 +0,0 @@ -@import "../../themes/ionic.globals"; - -// Select -// -------------------------------------------------- - -/// @prop - Margin top of the select popover list -$select-popover-list-margin-top: -1px !default; - -/// @prop - Margin end of the select popover list -$select-popover-list-margin-end: 0 !default; - -/// @prop - Margin bottom of the select popover list -$select-popover-list-margin-bottom: -1px !default; - -/// @prop - Margin start of the select popover list -$select-popover-list-margin-start: 0 !default; diff --git a/core/src/components/title/readme.md b/core/src/components/title/readme.md index 8b84b1a403..fd8a31e605 100644 --- a/core/src/components/title/readme.md +++ b/core/src/components/title/readme.md @@ -102,7 +102,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } @@ -240,7 +240,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } @@ -381,7 +381,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } @@ -544,7 +544,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } diff --git a/core/src/components/title/usage/angular.md b/core/src/components/title/usage/angular.md index 0f4967a927..dcfe393431 100644 --- a/core/src/components/title/usage/angular.md +++ b/core/src/components/title/usage/angular.md @@ -91,7 +91,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } diff --git a/core/src/components/title/usage/javascript.md b/core/src/components/title/usage/javascript.md index 3fe08dabef..384f5d5686 100644 --- a/core/src/components/title/usage/javascript.md +++ b/core/src/components/title/usage/javascript.md @@ -91,7 +91,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } diff --git a/core/src/components/title/usage/react.md b/core/src/components/title/usage/react.md index 8926887f95..cf404a8e30 100644 --- a/core/src/components/title/usage/react.md +++ b/core/src/components/title/usage/react.md @@ -127,7 +127,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } diff --git a/core/src/components/title/usage/stencil.md b/core/src/components/title/usage/stencil.md index 632595b0d9..a6f0c9f3df 100644 --- a/core/src/components/title/usage/stencil.md +++ b/core/src/components/title/usage/stencil.md @@ -130,7 +130,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } diff --git a/core/src/components/title/usage/vue.md b/core/src/components/title/usage/vue.md index 318656d126..408e1d83e1 100644 --- a/core/src/components/title/usage/vue.md +++ b/core/src/components/title/usage/vue.md @@ -152,7 +152,7 @@ You can change the background color of the toolbar with the standard title by se When styling the text color of the large title, you should target the large title globally as opposed to within the context of a particular page or tab, otherwise its styles will not be applied during the navigation animation. ```css -ion-title.large-title { +ion-title.title-large { color: purple; font-size: 30px; } diff --git a/core/src/utils/status-tap.ts b/core/src/utils/status-tap.ts index fbfe4f93c9..cc25d851c3 100644 --- a/core/src/utils/status-tap.ts +++ b/core/src/utils/status-tap.ts @@ -15,7 +15,21 @@ export const startStatusTap = () => { const contentEl = el.closest('ion-content'); if (contentEl) { new Promise(resolve => componentOnReady(contentEl, resolve)).then(() => { - writeTask(() => contentEl.scrollToTop(300)); + writeTask(async () => { + + /** + * If scrolling and user taps status bar, + * only calling scrollToTop is not enough + * as engines like WebKit will jump the + * scroll position back down and complete + * any in-progress momentum scrolling. + */ + contentEl.style.setProperty('--overflow', 'hidden'); + + await contentEl.scrollToTop(300); + + contentEl.style.removeProperty('--overflow'); + }); }); } }); diff --git a/packages/react/package.json b/packages/react/package.json index 6593e01f10..f9dca9acbf 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -52,6 +52,7 @@ "@rollup/plugin-virtual": "^2.0.3", "@testing-library/jest-dom": "^5.11.6", "@testing-library/react": "^11.2.2", + "@testing-library/react-hooks": "^7.0.1", "@types/jest": "^26.0.15", "@types/node": "^14.0.14", "@types/react": "16.14.0", diff --git a/packages/react/src/hooks/__tests__/hooks.spec.tsx b/packages/react/src/hooks/__tests__/hooks.spec.tsx new file mode 100644 index 0000000000..28a570b490 --- /dev/null +++ b/packages/react/src/hooks/__tests__/hooks.spec.tsx @@ -0,0 +1,153 @@ +import { alertController, modalController } from '@ionic/core'; + +import React from 'react'; + +import { useController } from '../useController'; +import { useOverlay } from '../useOverlay'; + +import { useIonActionSheet } from '../useIonActionSheet'; +import type { UseIonActionSheetResult } from '../useIonActionSheet'; +import { useIonAlert } from '../useIonAlert'; +import type { UseIonAlertResult } from '../useIonAlert'; +import { useIonLoading } from '../useIonLoading'; +import type { UseIonLoadingResult } from '../useIonLoading'; +import { useIonModal } from '../useIonModal'; +import type { UseIonModalResult } from '../useIonModal'; +import { useIonPicker } from '../useIonPicker'; +import type { UseIonPickerResult } from '../useIonPicker'; +import { useIonPopover } from '../useIonPopover'; +import type { UseIonPopoverResult } from '../useIonPopover'; +import { useIonToast } from '../useIonToast'; +import type { UseIonToastResult } from '../useIonToast'; + +import { renderHook } from '@testing-library/react-hooks'; + +describe('useController', () => { + it('should be memorised', () => { + const { result, rerender } = renderHook(() => + useController('AlertController', alertController) + ); + + rerender(); + + const [ + { present: firstPresent, dismiss: firstDismiss }, + { present: secondPresent, dismiss: secondDismiss }, + ] = result.all as ReturnType[]; + + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useIonActionSheet', () => { + it('should be memorised', () => { + const { result, rerender } = renderHook(() => useIonActionSheet()); + + rerender(); + + const [[firstPresent, firstDismiss], [secondPresent, secondDismiss]] = + result.all as UseIonActionSheetResult[]; + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useIonAlert', () => { + it('should be memorised', () => { + const { result, rerender } = renderHook(() => useIonAlert()); + + rerender(); + + const [[firstPresent, firstDismiss], [secondPresent, secondDismiss]] = + result.all as UseIonAlertResult[]; + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useIonLoading', () => { + it('should be memorised', () => { + const { result, rerender } = renderHook(() => useIonLoading()); + + rerender(); + + const [[firstPresent, firstDismiss], [secondPresent, secondDismiss]] = + result.all as UseIonLoadingResult[]; + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useIonModal', () => { + it('should be memorised', () => { + const ModalComponent = () =>
; + const { result, rerender } = renderHook(() => useIonModal(ModalComponent, {})); + + rerender(); + + const [[firstPresent, firstDismiss], [secondPresent, secondDismiss]] = + result.all as UseIonModalResult[]; + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useIonPicker', () => { + it('should be memorised', () => { + const { result, rerender } = renderHook(() => useIonPicker()); + + rerender(); + + const [[firstPresent, firstDismiss], [secondPresent, secondDismiss]] = + result.all as UseIonPickerResult[]; + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useIonPopover', () => { + it('should be memorised', () => { + const PopoverComponent = () =>
; + const { result, rerender } = renderHook(() => useIonPopover(PopoverComponent, {})); + + rerender(); + + const [[firstPresent, firstDismiss], [secondPresent, secondDismiss]] = + result.all as UseIonPopoverResult[]; + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useIonToast', () => { + it('should be memorised', () => { + const { result, rerender } = renderHook(() => useIonToast()); + + rerender(); + + const [[firstPresent, firstDismiss], [secondPresent, secondDismiss]] = + result.all as UseIonToastResult[]; + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); + +describe('useOverlay', () => { + it('should be memorised', () => { + const OverlayComponent = () =>
; + const { result, rerender } = renderHook(() => + useOverlay('IonModal', modalController, OverlayComponent, {}) + ); + + rerender(); + + const [ + { present: firstPresent, dismiss: firstDismiss }, + { present: secondPresent, dismiss: secondDismiss }, + ] = result.all as ReturnType[]; + + expect(firstPresent).toBe(secondPresent); + expect(firstDismiss).toBe(secondDismiss); + }); +}); diff --git a/packages/react/src/hooks/useController.ts b/packages/react/src/hooks/useController.ts index 44f8c45cfc..b43410f1bc 100644 --- a/packages/react/src/hooks/useController.ts +++ b/packages/react/src/hooks/useController.ts @@ -1,5 +1,5 @@ import { OverlayEventDetail } from '@ionic/core/components'; -import { useMemo, useRef } from 'react'; +import { useCallback, useMemo, useRef } from 'react'; import { attachProps } from '../components/react-component-lib/utils'; @@ -10,71 +10,53 @@ interface OverlayBase extends HTMLElement { dismiss: (data?: any, role?: string | undefined) => Promise; } -export function useController< - OptionsType, - OverlayType extends OverlayBase ->( +export function useController( displayName: string, controller: { create: (options: OptionsType) => Promise } ) { const overlayRef = useRef(); - const didDismissEventName = useMemo( - () => `on${displayName}DidDismiss`, - [displayName] - ); - const didPresentEventName = useMemo( - () => `on${displayName}DidPresent`, - [displayName] - ); - const willDismissEventName = useMemo( - () => `on${displayName}WillDismiss`, - [displayName] - ); - const willPresentEventName = useMemo( - () => `on${displayName}WillPresent`, - [displayName] - ); + const didDismissEventName = useMemo(() => `on${displayName}DidDismiss`, [displayName]); + const didPresentEventName = useMemo(() => `on${displayName}DidPresent`, [displayName]); + const willDismissEventName = useMemo(() => `on${displayName}WillDismiss`, [displayName]); + const willPresentEventName = useMemo(() => `on${displayName}WillPresent`, [displayName]); - const present = async (options: OptionsType & HookOverlayOptions) => { - if (overlayRef.current) { - return; - } - const { - onDidDismiss, - onWillDismiss, - onDidPresent, - onWillPresent, - ...rest - } = options; - - const handleDismiss = (event: CustomEvent>) => { - if (onDidDismiss) { - onDidDismiss(event); + const present = useCallback( + async (options: OptionsType & HookOverlayOptions) => { + if (overlayRef.current) { + return; } + const { onDidDismiss, onWillDismiss, onDidPresent, onWillPresent, ...rest } = options; + + const handleDismiss = (event: CustomEvent>) => { + if (onDidDismiss) { + onDidDismiss(event); + } + overlayRef.current = undefined; + }; + + overlayRef.current = await controller.create({ + ...(rest as any), + }); + + attachProps(overlayRef.current, { + [didDismissEventName]: handleDismiss, + [didPresentEventName]: (e: CustomEvent) => onDidPresent && onDidPresent(e), + [willDismissEventName]: (e: CustomEvent) => onWillDismiss && onWillDismiss(e), + [willPresentEventName]: (e: CustomEvent) => onWillPresent && onWillPresent(e), + }); + + overlayRef.current.present(); + }, + [controller] + ); + + const dismiss = useCallback( + () => async () => { + overlayRef.current && (await overlayRef.current.dismiss()); overlayRef.current = undefined; - } - - overlayRef.current = await controller.create({ - ...(rest as any), - }); - - attachProps(overlayRef.current, { - [didDismissEventName]: handleDismiss, - [didPresentEventName]: (e: CustomEvent) => - onDidPresent && onDidPresent(e), - [willDismissEventName]: (e: CustomEvent) => - onWillDismiss && onWillDismiss(e), - [willPresentEventName]: (e: CustomEvent) => - onWillPresent && onWillPresent(e), - }); - - overlayRef.current.present(); - }; - - const dismiss = async () => { - overlayRef.current && await overlayRef.current.dismiss(); - overlayRef.current = undefined; - }; + }, + [] + ); return { present, diff --git a/packages/react/src/hooks/useIonActionSheet.ts b/packages/react/src/hooks/useIonActionSheet.ts index ff9da0acfc..6827e6e590 100644 --- a/packages/react/src/hooks/useIonActionSheet.ts +++ b/packages/react/src/hooks/useIonActionSheet.ts @@ -1,4 +1,5 @@ import { ActionSheetButton, ActionSheetOptions, actionSheetController } from '@ionic/core/components'; +import { useCallback } from 'react'; import { HookOverlayOptions } from './HookOverlayOptions'; import { useController } from './useController'; @@ -13,23 +14,24 @@ export function useIonActionSheet(): UseIonActionSheetResult { actionSheetController ); - function present(buttons: ActionSheetButton[], header?: string): void; - function present(options: ActionSheetOptions & HookOverlayOptions): void; - function present(buttonsOrOptions: ActionSheetButton[] | ActionSheetOptions & HookOverlayOptions, header?: string) { - if (Array.isArray(buttonsOrOptions)) { - controller.present({ - buttons: buttonsOrOptions, - header - }); - } else { - controller.present(buttonsOrOptions); - } - } + const present = useCallback( + ( + buttonsOrOptions: ActionSheetButton[] | (ActionSheetOptions & HookOverlayOptions), + header?: string + ) => { + if (Array.isArray(buttonsOrOptions)) { + controller.present({ + buttons: buttonsOrOptions, + header, + }); + } else { + controller.present(buttonsOrOptions); + } + }, + [controller.present] + ); - return [ - present, - controller.dismiss - ]; + return [present, controller.dismiss]; } export type UseIonActionSheetResult = [ diff --git a/packages/react/src/hooks/useIonAlert.ts b/packages/react/src/hooks/useIonAlert.ts index 20c4208db5..e58b1105fd 100644 --- a/packages/react/src/hooks/useIonAlert.ts +++ b/packages/react/src/hooks/useIonAlert.ts @@ -1,4 +1,5 @@ import { AlertButton, AlertOptions, alertController } from '@ionic/core/components'; +import { useCallback } from 'react'; import { HookOverlayOptions } from './HookOverlayOptions'; import { useController } from './useController'; @@ -8,28 +9,23 @@ import { useController } from './useController'; * @returns Returns the present and dismiss methods in an array */ export function useIonAlert(): UseIonAlertResult { - const controller = useController( - 'IonAlert', - alertController + const controller = useController('IonAlert', alertController); + + const present = useCallback( + (messageOrOptions: string | (AlertOptions & HookOverlayOptions), buttons?: AlertButton[]) => { + if (typeof messageOrOptions === 'string') { + controller.present({ + message: messageOrOptions, + buttons: buttons ?? [{ text: 'Ok' }], + }); + } else { + controller.present(messageOrOptions); + } + }, + [controller.present] ); - function present(message: string, buttons?: AlertButton[]): void; - function present(options: AlertOptions & HookOverlayOptions): void; - function present(messageOrOptions: string | AlertOptions & HookOverlayOptions, buttons?: AlertButton[]) { - if (typeof messageOrOptions === 'string') { - controller.present({ - message: messageOrOptions, - buttons: buttons ?? [{ text: 'Ok' }] - }); - } else { - controller.present(messageOrOptions); - } - }; - - return [ - present, - controller.dismiss - ]; + return [present, controller.dismiss]; } export type UseIonAlertResult = [ diff --git a/packages/react/src/hooks/useIonLoading.tsx b/packages/react/src/hooks/useIonLoading.tsx index 751d174a12..9a0a9bb1ad 100644 --- a/packages/react/src/hooks/useIonLoading.tsx +++ b/packages/react/src/hooks/useIonLoading.tsx @@ -1,4 +1,5 @@ import { LoadingOptions, SpinnerTypes, loadingController } from '@ionic/core/components'; +import { useCallback } from 'react'; import { HookOverlayOptions } from './HookOverlayOptions'; import { useController } from './useController'; @@ -13,27 +14,24 @@ export function useIonLoading(): UseIonLoadingResult { loadingController ); - function present( - message?: string, - duration?: number, - spinner?: SpinnerTypes - ): void; - function present(options: LoadingOptions & HookOverlayOptions): void; - function present( - messageOrOptions: string | (LoadingOptions & HookOverlayOptions) = '', - duration?: number, - spinner?: SpinnerTypes - ) { - if (typeof messageOrOptions === 'string') { - controller.present({ - message: messageOrOptions, - duration, - spinner: spinner ?? 'lines', - }); - } else { - controller.present(messageOrOptions); - } - } + const present = useCallback( + ( + messageOrOptions: string | (LoadingOptions & HookOverlayOptions) = '', + duration?: number, + spinner?: SpinnerTypes + ) => { + if (typeof messageOrOptions === 'string') { + controller.present({ + message: messageOrOptions, + duration, + spinner: spinner ?? 'lines', + }); + } else { + controller.present(messageOrOptions); + } + }, + [controller.present] + ); return [present, controller.dismiss]; } diff --git a/packages/react/src/hooks/useIonModal.ts b/packages/react/src/hooks/useIonModal.ts index b5ba4282ff..88e7c65ad9 100644 --- a/packages/react/src/hooks/useIonModal.ts +++ b/packages/react/src/hooks/useIonModal.ts @@ -1,4 +1,5 @@ import { ModalOptions, modalController } from '@ionic/core/components'; +import { useCallback } from 'react'; import { HookOverlayOptions } from './HookOverlayOptions'; import { ReactComponentOrElement, useOverlay } from './useOverlay'; @@ -9,7 +10,10 @@ import { ReactComponentOrElement, useOverlay } from './useOverlay'; * @param componentProps The props that will be passed to the component, if required * @returns Returns the present and dismiss methods in an array */ -export function useIonModal(component: ReactComponentOrElement, componentProps?: any): UseIonModalResult { +export function useIonModal( + component: ReactComponentOrElement, + componentProps?: any +): UseIonModalResult { const controller = useOverlay( 'IonModal', modalController, @@ -17,14 +21,14 @@ export function useIonModal(component: ReactComponentOrElement, componentProps?: componentProps ); - function present(options: Omit & HookOverlayOptions = {}) { - controller.present(options as any); - }; + const present = useCallback( + (options: Omit & HookOverlayOptions = {}) => { + controller.present(options as any); + }, + [controller.present] + ); - return [ - present, - controller.dismiss - ]; + return [present, controller.dismiss]; } export type UseIonModalResult = [ diff --git a/packages/react/src/hooks/useIonPicker.tsx b/packages/react/src/hooks/useIonPicker.tsx index 6eb9f1b5b7..cd9adb897c 100644 --- a/packages/react/src/hooks/useIonPicker.tsx +++ b/packages/react/src/hooks/useIonPicker.tsx @@ -4,6 +4,7 @@ import { PickerOptions, pickerController, } from '@ionic/core/components'; +import { useCallback } from 'react'; import { HookOverlayOptions } from './HookOverlayOptions'; import { useController } from './useController'; @@ -18,12 +19,10 @@ export function useIonPicker(): UseIonPickerResult { pickerController ); - function present(columns: PickerColumn[], buttons?: PickerButton[]): void; - function present(options: PickerOptions & HookOverlayOptions): void; - function present( + const present = useCallback(( columnsOrOptions: PickerColumn[] | (PickerOptions & HookOverlayOptions), buttons?: PickerButton[] - ) { + ) => { if (Array.isArray(columnsOrOptions)) { controller.present({ columns: columnsOrOptions, @@ -32,7 +31,7 @@ export function useIonPicker(): UseIonPickerResult { } else { controller.present(columnsOrOptions); } - } + }, [controller.present]); return [present, controller.dismiss]; } diff --git a/packages/react/src/hooks/useIonPopover.ts b/packages/react/src/hooks/useIonPopover.ts index f0efd037c0..a9b412334b 100644 --- a/packages/react/src/hooks/useIonPopover.ts +++ b/packages/react/src/hooks/useIonPopover.ts @@ -1,4 +1,5 @@ import { PopoverOptions, popoverController } from '@ionic/core/components'; +import { useCallback } from 'react'; import { HookOverlayOptions } from './HookOverlayOptions'; import { ReactComponentOrElement, useOverlay } from './useOverlay'; @@ -17,9 +18,9 @@ export function useIonPopover(component: ReactComponentOrElement, componentProps componentProps ); - function present(options: Omit & HookOverlayOptions = {}) { + const present = useCallback((options: Omit & HookOverlayOptions = {}) => { controller.present(options as any); - }; + }, [controller.present]); return [ present, diff --git a/packages/react/src/hooks/useIonToast.ts b/packages/react/src/hooks/useIonToast.ts index 0d22842ca3..e56f8c9ee8 100644 --- a/packages/react/src/hooks/useIonToast.ts +++ b/packages/react/src/hooks/useIonToast.ts @@ -1,4 +1,5 @@ import { ToastOptions, toastController } from '@ionic/core/components'; +import { useCallback } from 'react'; import { HookOverlayOptions } from './HookOverlayOptions'; import { useController } from './useController'; @@ -13,9 +14,7 @@ export function useIonToast(): UseIonToastResult { toastController ); - function present(message: string, duration?: number): void; - function present(options: ToastOptions & HookOverlayOptions): void; - function present(messageOrOptions: string | ToastOptions & HookOverlayOptions, duration?: number) { + const present = useCallback((messageOrOptions: string | ToastOptions & HookOverlayOptions, duration?: number) => { if (typeof messageOrOptions === 'string') { controller.present({ message: messageOrOptions, @@ -24,7 +23,7 @@ export function useIonToast(): UseIonToastResult { } else { controller.present(messageOrOptions); } - }; + }, [controller.present]); return [ present, diff --git a/packages/react/src/hooks/useOverlay.ts b/packages/react/src/hooks/useOverlay.ts index ab23262be2..31d6d622e6 100644 --- a/packages/react/src/hooks/useOverlay.ts +++ b/packages/react/src/hooks/useOverlay.ts @@ -1,5 +1,5 @@ import { OverlayEventDetail } from '@ionic/core/components'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; import { attachProps } from '../components/react-component-lib/utils'; @@ -52,7 +52,7 @@ export function useOverlay< } }, [component, containerElRef.current, isOpen, componentProps]); - const present = async (options: OptionsType & HookOverlayOptions) => { + const present = useCallback(async (options: OptionsType & HookOverlayOptions) => { if (overlayRef.current) { return; } @@ -96,13 +96,13 @@ export function useOverlay< containerElRef.current = undefined; setIsOpen(false); } - }; + }, []); - const dismiss = async () => { + const dismiss = useCallback(async () => { overlayRef.current && await overlayRef.current.dismiss(); overlayRef.current = undefined; containerElRef.current = undefined; - }; + }, []); return { present,