chore(all): sync with main for beta 7

This commit is contained in:
Liam DeBeasi
2021-10-06 10:52:14 -04:00
39 changed files with 438 additions and 194 deletions

View File

@ -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).

View File

@ -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).

View File

@ -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)

View File

@ -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'

View File

@ -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();

View File

@ -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

View File

@ -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<boolean>;
/**
* 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<RouterEventDetail>) => 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;
/**

View File

@ -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;

View File

@ -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,

View File

@ -41,6 +41,7 @@
<ion-button expand="block" onclick="openItem('start')">Open Item Start</ion-button>
<ion-button expand="block" onclick="openItem('end')">Open Item End</ion-button>
<ion-button expand="block" onclick="openItemOneSide()">Open Item with only one side</ion-button>
<ion-button expand="block" onclick="setDynaicItem()">Swap dynamic item</ion-button>
</div>
<ion-list id="list">
@ -369,6 +370,17 @@
</ion-item-options>
</ion-item-sliding>
<ion-item-sliding id="dynamic-item">
<ion-item>
<ion-label>Dynamic First Item</ion-label>
</ion-item>
<ion-item-options side="end">
<ion-item-option color="tertiary" expandable>
First Item Options
</ion-item-option>
</ion-item-options>
</ion-item-sliding>
<ion-item>
<ion-label class="ion-text-wrap">
<h2>Normal ion-item (no sliding)</h2>
@ -387,6 +399,20 @@
</ion-list>
<script>
const setDynaicItem = () => {
const sliding = document.querySelector('#dynamic-item');
sliding.innerHTML = `
<ion-item>
<ion-label>Dynamic Second Item</ion-label>
</ion-item>
<ion-item-options side="end">
<ion-item-option color="tertiary" expandable>
Second Item Options
</ion-item-option>
</ion-item-options>
`
}
var dynamicSlidingEnabled = document.getElementsByClassName('sliding-enabled');
// Toggle the dynamic options

View File

@ -89,11 +89,10 @@
color: #{$item-ios-paragraph-text-color};
}
:host-context(.ion-color)::slotted(p) {
:host(.in-item-color)::slotted(p) {
color: inherit;
}
::slotted(*) h2:last-child,
::slotted(*) h3:last-child,
::slotted(*) h4:last-child,

View File

@ -188,6 +188,6 @@
color: $item-md-paragraph-text-color;
}
:host-context(.ion-color)::slotted(p) {
:host(.in-item-color)::slotted(p) {
color: inherit;
}

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Host, Prop
import { getIonMode } from '../../global/ionic-global';
import { Color, StyleEventDetail } from '../../interface';
import { createColorClasses } from '../../utils/theme';
import { createColorClasses, hostContext } from '../../utils/theme';
/**
* @virtualProp {"ios" | "md"} mode - The mode determines which platform styles to use.
@ -101,6 +101,7 @@ export class Label implements ComponentInterface {
<Host
class={createColorClasses(this.color, {
[mode]: true,
'in-item-color': hostContext('ion-item.ion-color', this.el),
[`label-${position}`]: position !== undefined,
[`label-no-animate`]: (this.noAnimate),
'label-rtl': document.dir === 'rtl'

View File

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

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="UTF-8">
<title>Label - Color</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>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Label - Color</ion-title>
</ion-toolbar>
</ion-header>
<ion-content color="light">
<ion-item>
<ion-label>Label Text<p>This paragraph should not inherit the color from content</p></ion-label>
</ion-item>
</ion-content>
</ion-app>
</body>
</html>

View File

@ -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

View File

@ -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.
*/

View File

@ -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` |

View File

@ -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 = '/';

View File

@ -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,

View File

@ -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;

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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');
});
});
}
});

View File

@ -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",

View File

@ -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<typeof useController>[];
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 = () => <div />;
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 = () => <div />;
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 = () => <div />;
const { result, rerender } = renderHook(() =>
useOverlay('IonModal', modalController, OverlayComponent, {})
);
rerender();
const [
{ present: firstPresent, dismiss: firstDismiss },
{ present: secondPresent, dismiss: secondDismiss },
] = result.all as ReturnType<typeof useOverlay>[];
expect(firstPresent).toBe(secondPresent);
expect(firstDismiss).toBe(secondDismiss);
});
});

View File

@ -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<boolean>;
}
export function useController<
OptionsType,
OverlayType extends OverlayBase
>(
export function useController<OptionsType, OverlayType extends OverlayBase>(
displayName: string,
controller: { create: (options: OptionsType) => Promise<OverlayType> }
) {
const overlayRef = useRef<OverlayType>();
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<OverlayEventDetail<any>>) => {
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<OverlayEventDetail<any>>) => {
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,

View File

@ -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 = [

View File

@ -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<AlertOptions, HTMLIonAlertElement>(
'IonAlert',
alertController
const controller = useController<AlertOptions, HTMLIonAlertElement>('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 = [

View File

@ -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];
}

View File

@ -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<ModalOptions, HTMLIonModalElement>(
'IonModal',
modalController,
@ -17,14 +21,14 @@ export function useIonModal(component: ReactComponentOrElement, componentProps?:
componentProps
);
function present(options: Omit<ModalOptions, 'component' | 'componentProps'> & HookOverlayOptions = {}) {
controller.present(options as any);
};
const present = useCallback(
(options: Omit<ModalOptions, 'component' | 'componentProps'> & HookOverlayOptions = {}) => {
controller.present(options as any);
},
[controller.present]
);
return [
present,
controller.dismiss
];
return [present, controller.dismiss];
}
export type UseIonModalResult = [

View File

@ -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];
}

View File

@ -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<PopoverOptions, 'component' | 'componentProps'> & HookOverlayOptions = {}) {
const present = useCallback((options: Omit<PopoverOptions, 'component' | 'componentProps'> & HookOverlayOptions = {}) => {
controller.present(options as any);
};
}, [controller.present]);
return [
present,

View File

@ -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,

View File

@ -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,