Compare commits

..

32 Commits

Author SHA1 Message Date
Liam DeBeasi
926b119231 remove old lines 2019-11-18 15:03:42 -05:00
Liam DeBeasi
256c7de469 only fire pressUp if press was fired; allow overriding of other gesture params 2019-11-18 15:02:38 -05:00
Liam DeBeasi
b7b5b94cce add angular integration 2019-11-12 11:39:21 -05:00
Liam DeBeasi
5e1e883585 sync with master 2019-11-12 11:33:42 -05:00
Liam DeBeasi
21484f1f3a fix(gesture): release gesture when disabling (#19855)
fixes #19848
2019-11-12 10:45:06 -05:00
Tawfiek Khalaf
b28cf02ef3 feat(alert): add support for textarea (#16851)
resolves #14153
2019-11-11 14:27:45 -05:00
Liam DeBeasi
bef0f53d0d feat(select): add --placeholder-opacity and --placeholder-color, expose shadow parts (#19893)
resolves #17446
2019-11-11 12:49:10 -05:00
Brandy Carney
1a8b7a4559 fix(card): update background to use the same as item (#19602)
uses item background but falls back to the background of the content
2019-11-08 12:55:19 -05:00
Brandy Carney
b23c759456 docs(slides): document css vars (#19872) 2019-11-08 12:37:00 -05:00
Antoine
1ab7066aa0 feat(searchbar): add --box-shadow variable to style searchbar input (#19838)
* Updates searchbar.md.scss to expose box-shadow

We would like to style the box-shadow property in searchbar and give the parent the ability to control it.

* fix(searchbar): add box shadow to main input for both modes
2019-11-08 10:53:42 -05:00
Liam DeBeasi
48a766246d feat(angular): expose Ionic Gestures via GestureController (#19864)
* expose gestures thru angular

* run linter
2019-11-08 10:35:52 -05:00
Brandy Carney
fce3e24600 fix(content): set fixed content to position absolute (#19867)
Adds absolute position to fixed slot in order to display text that is inside of the slot.

Fixes #17754

Co-authored-by: Matthew de Nobrega matthew@caro.health
2019-11-08 10:31:33 -05:00
Masahiko Sakakibara
be0ab5c2ff chore(menu): change deprecated error message (#19717) 2019-11-07 18:58:32 -05:00
Simon
c5be8831a2 docs(searchbar): update vue usage to kebab-case for the attributes (#19625) 2019-11-07 18:55:47 -05:00
Ely Lucas
edf64ef121 merge release-4.11.4 2019-11-07 15:54:48 -07:00
Himujjal Upadhyaya
504051d709 fix(nav-params): set generic type on navigation parameters get() (#19195) 2019-11-07 17:45:31 -05:00
Dylan Ballandras
90be7ec4b6 docs(toggle): update prop name for toggle OnChange in react (#19344)
closes #19343
2019-11-07 17:36:37 -05:00
Ely Lucas
1af1e2a14f chore(core): fixing test from bad rebase 2019-11-07 15:32:39 -07:00
Ely Lucas
2cc4244c05 4.11.4 2019-11-07 15:26:04 -07:00
Aubrey Holland
74e40cdc35 fix(react): check for component unmount, fixes #19859 2019-11-07 15:24:52 -07:00
Ely Lucas
13b323f25d chore(react): lowering the timeout for the ionpage check to avoid false positives 2019-11-07 15:24:52 -07:00
Ely Lucas
3e14a57f84 fix(react): adding multiple subscriptions to lifecycle events, fixes #19792 (#19858) 2019-11-07 15:24:52 -07:00
Ely Lucas
9864c17c1c fix(react): add check to warn if no ionpage is found, fixes #19832 (#19857) 2019-11-07 15:24:52 -07:00
Ely Lucas
e2ed0e9e87 fix(react): expand the location stack to better support back button, fixes #19748 (#19856) 2019-11-07 15:24:52 -07:00
Ely Lucas
a4b2de5730 fix(react): adding hardware back button support, fixes(19819) (#19851) 2019-11-07 15:24:52 -07:00
Ely Lucas
f3ac682010 fix(react): adding swipe back functionality and routerOutlet ready improvements, fixes #19818 (#19849) 2019-11-07 15:24:52 -07:00
Ely Lucas
2f5d823748 fix(react): create a new overlay each time component is presented, fixes #19841, #19823 (#19842) 2019-11-07 15:24:52 -07:00
Liam DeBeasi
e5e9dd5dfe fix(menu): clamp out of bounds swipe value (#19684)
fixes #18927
2019-11-07 15:24:09 -07:00
Liam DeBeasi
ad747b05f1 add press up handler 2019-11-07 15:20:49 -05:00
Liam DeBeasi
3b52074a10 fix typo 2019-11-07 15:17:26 -05:00
Liam DeBeasi
9eac7266a0 Fix 2019-11-07 15:04:18 -05:00
Liam DeBeasi
12a4c974b4 add press recognizer 2019-11-07 15:03:05 -05:00
52 changed files with 761 additions and 217 deletions

View File

@@ -1,3 +1,17 @@
## [4.11.4](https://github.com/ionic-team/ionic/compare/v4.11.1...v4.11.4) (2019-11-07)
### Bug Fixes
* **react:** check for component unmount, fixes [#19859](https://github.com/ionic-team/ionic/issues/19859) ([7356c40](https://github.com/ionic-team/ionic/commit/7356c401742ce2b3241d6ab05fce0fa65d2f1f8a))
* **react:** adding multiple subscriptions to lifecycle events, fixes [#19792](https://github.com/ionic-team/ionic/issues/19792) ([#19858](https://github.com/ionic-team/ionic/issues/19858)) ([0a3014d](https://github.com/ionic-team/ionic/commit/0a3014d35e2102570fd3d8c5ada29eb01aab18e9))
* **react:** add check to warn if no ionpage is found, fixes [#19832](https://github.com/ionic-team/ionic/issues/19832) ([#19857](https://github.com/ionic-team/ionic/issues/19857)) ([628e766](https://github.com/ionic-team/ionic/commit/628e76668ea72baebdb02b9dcfe24c0da837fb08))
* **react:** expand the location stack to better support back button, fixes [#19748](https://github.com/ionic-team/ionic/issues/19748) ([#19856](https://github.com/ionic-team/ionic/issues/19856)) ([d89508b](https://github.com/ionic-team/ionic/commit/d89508b1b58481d518b89362a8792d05f3f451c9))
* **react:** adding hardware back button support, fixes(19819) ([#19851](https://github.com/ionic-team/ionic/issues/19851)) ([fd9745d](https://github.com/ionic-team/ionic/commit/fd9745ddcddded76d64220838aef0f599bf4352f))
* **react:** adding swipe back functionality and routerOutlet ready improvements, fixes [#19818](https://github.com/ionic-team/ionic/issues/19818) ([#19849](https://github.com/ionic-team/ionic/issues/19849)) ([bcc40c8](https://github.com/ionic-team/ionic/commit/bcc40c8d59b723bbdb1dfd318bfb2219eb8df3cf))
* **react:** create a new overlay each time component is presented, fixes [#19841](https://github.com/ionic-team/ionic/issues/19841), [#19823](https://github.com/ionic-team/ionic/issues/19823) ([#19842](https://github.com/ionic-team/ionic/issues/19842)) ([9fad416](https://github.com/ionic-team/ionic/commit/9fad4161be4859969e14d4d33169ef022052d6bf))
## [4.11.3](https://github.com/ionic-team/ionic/compare/v4.11.1...v4.11.3) (2019-10-30)

View File

@@ -38,7 +38,7 @@ export class NavParams {
*
* @param param Which param you want to look up
*/
get(param: string): any {
get<T = any>(param: string): T {
return this.data[param];
}
}

View File

@@ -31,6 +31,7 @@ export { NavController } from './providers/nav-controller';
export { DomController } from './providers/dom-controller';
export { Config } from './providers/config';
export { AnimationController } from './providers/animation-controller';
export { GestureController } from './providers/gesture-controller';
// ROUTER STRATEGY
export { IonicRouteStrategy } from './util/ionic-router-reuse-strategy';

View File

@@ -0,0 +1,21 @@
import { Injectable } from '@angular/core';
import { Gesture, GestureConfig, PressRecognizerOptions, createGesture, createPressRecognizer } from '@ionic/core';
@Injectable({
providedIn: 'root',
})
export class GestureController {
/**
* Create a new gesture
*/
create(opts: GestureConfig): Gesture {
return createGesture(opts);
}
/**
* Create a new Press recognizer gesture
*/
pressRecognizer(opts: PressRecognizerOptions): Gesture {
return createPressRecognizer(opts);
}
}

View File

@@ -965,6 +965,7 @@ ion-searchbar,event,ionClear,void,true
ion-searchbar,event,ionFocus,void,true
ion-searchbar,event,ionInput,KeyboardEvent,true
ion-searchbar,css-prop,--background
ion-searchbar,css-prop,--box-shadow
ion-searchbar,css-prop,--cancel-button-color
ion-searchbar,css-prop,--clear-button-color
ion-searchbar,css-prop,--color
@@ -1037,6 +1038,8 @@ ion-select,css-prop,--padding-bottom
ion-select,css-prop,--padding-end
ion-select,css-prop,--padding-start
ion-select,css-prop,--padding-top
ion-select,css-prop,--placeholder-color
ion-select,css-prop,--placeholder-opacity
ion-select-option,shadow
ion-select-option,prop,disabled,boolean,false,false,false
@@ -1090,6 +1093,10 @@ ion-slides,event,ionSlideTransitionStart,void,true
ion-slides,event,ionSlideWillChange,void,true
ion-slides,css-prop,--bullet-background
ion-slides,css-prop,--bullet-background-active
ion-slides,css-prop,--progress-bar-background
ion-slides,css-prop,--progress-bar-background-active
ion-slides,css-prop,--scroll-bar-background
ion-slides,css-prop,--scroll-bar-background-active
ion-spinner,shadow
ion-spinner,prop,color,string | undefined,undefined,false,false

View File

@@ -20,7 +20,7 @@ export interface AlertOptions {
}
export interface AlertInput {
type?: TextFieldTypes | 'checkbox' | 'radio';
type?: TextFieldTypes | 'checkbox' | 'radio' | 'textarea';
name?: string;
placeholder?: string;
value?: any;

View File

@@ -197,3 +197,8 @@
.alert-checkbox-inner {
box-sizing: border-box;
}
textarea.alert-input {
min-height: $alert-input-min-height;
resize: none;
}

View File

@@ -373,25 +373,46 @@ export class Alert implements ComponentInterface, OverlayInterface {
}
return (
<div class="alert-input-group" aria-labelledby={labelledby}>
{ inputs.map(i => (
<div class="alert-input-wrapper">
<input
placeholder={i.placeholder}
value={i.value}
type={i.type}
min={i.min}
max={i.max}
onInput={e => i.value = (e.target as any).value}
id={i.id}
disabled={i.disabled}
tabIndex={0}
class={{
'alert-input': true,
'alert-input-disabled': i.disabled || false
}}
/>
</div>
))}
{ inputs.map(i => {
if (i.type === 'textarea') {
return (
<div class="alert-input-wrapper">
<textarea
placeholder={i.placeholder}
value={i.value}
onInput={e => i.value = (e.target as any).value}
id={i.id}
disabled={i.disabled}
tabIndex={0}
class={{
'alert-input': true,
'alert-input-disabled': i.disabled || false
}}
/>
</div>
);
} else {
return (
<div class="alert-input-wrapper">
<input
placeholder={i.placeholder}
value={i.value}
type={i.type}
min={i.min}
max={i.max}
onInput={e => i.value = (e.target as any).value}
id={i.id}
disabled={i.disabled}
tabIndex={0}
class={{
'alert-input': true,
'alert-input-disabled': i.disabled || false
}}
/>
</div>
);
}
})}
</div>
);
}

View File

@@ -14,3 +14,6 @@ $alert-button-line-height: 20px !default;
/// @prop - Font size of the alert button
$alert-button-font-size: 14px !default;
/// @prop - Minimum height of a textarea in the alert
$alert-input-min-height: 37px !default;

View File

@@ -17,7 +17,7 @@ Optionally, a `role` property can be added to a button, such as `cancel`. If a `
### Inputs
Alerts can also include several different inputs whose data can be passed back to the app. Inputs can be used as a simple way to prompt users for information. Radios, checkboxes and text inputs are all accepted, but they cannot be mixed. For example, an alert could have all radio button inputs, or all checkbox inputs, but the same alert cannot mix radio and checkbox inputs. Do note however, different types of "text" inputs can be mixed, such as `url`, `email`, `text`, etc. If you require a complex form UI which doesn't fit within the guidelines of an alert then we recommend building the form within a modal instead.
Alerts can also include several different inputs whose data can be passed back to the app. Inputs can be used as a simple way to prompt users for information. Radios, checkboxes and text inputs are all accepted, but they cannot be mixed. For example, an alert could have all radio button inputs, or all checkbox inputs, but the same alert cannot mix radio and checkbox inputs. Do note however, different types of "text" inputs can be mixed, such as `url`, `email`, `text`, `textarea` etc. If you require a complex form UI which doesn't fit within the guidelines of an alert then we recommend building the form within a modal instead.
<!-- Auto Generated Below -->
@@ -102,6 +102,13 @@ export class AlertExample {
value: 'hello',
placeholder: 'Placeholder 2'
},
// multiline input.
{
name: 'paragraph',
id: 'paragraph',
type: 'textarea',
placeholder: 'Placeholder 3'
},
{
name: 'name3',
value: 'http://ionicframework.com',
@@ -346,6 +353,13 @@ function presentAlertPrompt() {
value: 'hello',
placeholder: 'Placeholder 2'
},
// multiline input.
{
name: 'paragraph',
id: 'paragraph',
type: 'textarea',
placeholder: 'Placeholder 3'
},
{
name: 'name3',
value: 'http://ionicframework.com',

View File

@@ -131,6 +131,11 @@
placeholder: 'Placeholder 3',
disabled: true
},
{
type: 'textarea',
placeholder: 'Placeholder 4',
value: 'Textarea hello'
},
{
name: 'name3',
type: 'text',

View File

View File

@@ -73,6 +73,13 @@ export class AlertExample {
value: 'hello',
placeholder: 'Placeholder 2'
},
// multiline input.
{
name: 'paragraph',
id: 'paragraph',
type: 'textarea',
placeholder: 'Placeholder 3'
},
{
name: 'name3',
value: 'http://ionicframework.com',

View File

@@ -58,6 +58,13 @@ function presentAlertPrompt() {
value: 'hello',
placeholder: 'Placeholder 2'
},
// multiline input.
{
name: 'paragraph',
id: 'paragraph',
type: 'textarea',
placeholder: 'Placeholder 3'
},
{
name: 'name3',
value: 'http://ionicframework.com',

View File

@@ -5,7 +5,7 @@
// --------------------------------------------------
:host {
--background: var(--ion-item-background, transparent);
--background: #{$item-ios-background};
--color: #{$card-ios-text-color};
@include margin($card-ios-margin-top, $card-ios-margin-end, $card-ios-margin-bottom, $card-ios-margin-start);

View File

@@ -5,7 +5,7 @@
// --------------------------------------------------
:host {
--background: var(--ion-item-background, transparent);
--background: #{$item-md-background};
--color: #{$card-md-text-color};
@include margin($card-md-margin-top, $card-md-margin-end, $card-md-margin-bottom, $card-md-margin-start);

View File

@@ -130,7 +130,7 @@
height: 100%;
opacity: 0;
pointer-events: none;
}
@@ -164,3 +164,11 @@
background-repeat: repeat-y;
background-size: 10px 16px;
}
// Content: Fixed
// --------------------------------------------------
::slotted([slot="fixed"]) {
position: absolute;
}

View File

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

View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Content - Fixed</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></head>
<body>
<ion-app>
<ion-header id="header">
<ion-toolbar>
<ion-title>Content - Fixed</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<h1>Regular Content</h1>
<div slot="fixed">
<h1>Fixed content</h1>
<p>Fixed paragraph</p>
<ion-button>Button</ion-button>
</div>
<ion-fab vertical="top" horizontal="end" slot="fixed">
<ion-fab-button>
<ion-icon name="add"></ion-icon>
</ion-fab-button>
</ion-fab>
<ion-fab vertical="bottom" horizontal="start" slot="fixed">
<ion-fab-button color="tertiary">
<ion-icon name="star"></ion-icon>
</ion-fab-button>
</ion-fab>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -163,8 +163,8 @@ BEFORE:
<div main>...</div>
AFTER:
<ion-menu contentId="my-content"></ion-menu>
<div id="my-content">...</div>
<ion-menu contentId="main-content"></ion-menu>
<div id="main-content">...</div>
`);
}
const content = this.contentId !== undefined

View File

@@ -171,15 +171,15 @@ export const SearchbarExample: React.FC = () => (
<template>
<!-- Default Searchbar -->
<ion-searchbar></ion-searchbar>
<!-- Searchbar with cancel button always shown -->
<ion-searchbar showCancelButton="always"></ion-searchbar>
<ion-searchbar show-cancel-button="always"></ion-searchbar>
<!-- Searchbar with cancel button never shown -->
<ion-searchbar showCancelButton="never"></ion-searchbar>
<ion-searchbar show-cancel-button="never"></ion-searchbar>
<!-- Searchbar with cancel button shown on focus -->
<ion-searchbar showCancelButton="focus"></ion-searchbar>
<ion-searchbar show-cancel-button="focus"></ion-searchbar>
<!-- Searchbar with danger color -->
<ion-searchbar color="danger"></ion-searchbar>
@@ -197,7 +197,7 @@ export const SearchbarExample: React.FC = () => (
<ion-searchbar disabled="true"></ion-searchbar>
<!-- Searchbar with a cancel button and custom cancel button text -->
<ion-searchbar showCancelButton="focus" cancelButtonText="Custom Cancel"></ion-searchbar>
<ion-searchbar show-cancel-button="focus" cancel-button-text="Custom Cancel"></ion-searchbar>
<!-- Searchbar with a custom debounce -->
<ion-searchbar debounce="500"></ion-searchbar>
@@ -280,7 +280,8 @@ Type: `Promise<void>`
| Name | Description |
| --------------------------- | ---------------------------------------- |
| `--background` | Background of the searchbar |
| `--background` | Background of the searchbar input |
| `--box-shadow` | Box shadow of the searchbar input |
| `--cancel-button-color` | Color of the searchbar cancel button |
| `--clear-button-color` | Color of the searchbar clear button |
| `--color` | Color of the searchbar text |

View File

@@ -5,11 +5,12 @@
// --------------------------------------------------
:host {
--clear-button-color: #{$searchbar-ios-input-clear-icon-color};
--background: #{$searchbar-ios-input-background-color};
--box-shadow: none;
--cancel-button-color: #{$searchbar-ios-cancel-button-color};
--clear-button-color: #{$searchbar-ios-input-clear-icon-color};
--color: #{$searchbar-ios-input-text-color};
--icon-color: #{$searchbar-ios-input-search-icon-color};
--background: #{$searchbar-ios-input-background-color};
@include padding($searchbar-ios-padding-top, $searchbar-ios-padding-end, $searchbar-ios-padding-bottom, $searchbar-ios-padding-start);
@@ -20,8 +21,6 @@
.searchbar-input-container {
height: $searchbar-ios-input-height;
contain: strict;
}

View File

@@ -5,11 +5,12 @@
// --------------------------------------------------
:host {
--clear-button-color: initial;
--background: #{$searchbar-md-input-background-color};
--box-shadow: #{$searchbar-md-input-box-shadow};
--cancel-button-color: #{$searchbar-md-cancel-button-color};
--clear-button-color: initial;
--color: #{$searchbar-md-input-text-color};
--icon-color: #{$searchbar-md-input-search-icon-color};
--background: #{$searchbar-md-input-background-color};
@include padding($searchbar-md-padding-top, $searchbar-md-padding-end, $searchbar-md-padding-bottom, $searchbar-md-padding-start);
@@ -67,8 +68,6 @@
font-weight: 400;
line-height: $searchbar-md-input-line-height;
box-shadow: $searchbar-md-input-box-shadow;
}

View File

@@ -5,7 +5,8 @@
:host {
/**
* @prop --background: Background of the searchbar
* @prop --background: Background of the searchbar input
* @prop --box-shadow: Box shadow of the searchbar input
* @prop --cancel-button-color: Color of the searchbar cancel button
* @prop --clear-button-color: Color of the searchbar clear button
* @prop --color: Color of the searchbar text
@@ -68,8 +69,6 @@
.searchbar-input {
@include text-inherit();
box-sizing: border-box;
display: block;
width: 100%;
@@ -81,6 +80,10 @@
background: var(--background);
font-family: inherit;
box-shadow: var(--box-shadow);
box-sizing: border-box;
appearance: none;
&::placeholder {

View File

@@ -26,7 +26,7 @@
</ion-searchbar>
<h5 class="ion-padding-start ion-padding-top"> Search - No Cancel Button </h5>
<ion-searchbar id="noCancel" value="after view" autocorrect="off" autocomplete="off" spellcheck="true" type="text" show-cancel-button="never">
<ion-searchbar class="red-box" id="noCancel" value="after view" autocorrect="off" autocomplete="off" spellcheck="true" type="text" show-cancel-button="never">
</ion-searchbar>
<h5 class="ion-padding-start ion-padding-top"> Search - Always Show Cancel Button</h5>
@@ -118,6 +118,12 @@
<ion-button expand="block" color="secondary" onClick="toggleDisabled()">Toggle disabled</ion-button>
</div>
<style>
.red-box {
--box-shadow: 0 2px 2px 0 rgba(255, 0, 0, 0.14), 0 3px 1px -2px rgba(255, 0, 0, 0.2), 0 1px 5px 0 rgba(255, 0, 0, 0.12);
}
</style>
<script>
function toggleAttr() {
var dynamicAttr = document.getElementById('dynamicAttr');

View File

@@ -2,15 +2,15 @@
<template>
<!-- Default Searchbar -->
<ion-searchbar></ion-searchbar>
<!-- Searchbar with cancel button always shown -->
<ion-searchbar showCancelButton="always"></ion-searchbar>
<ion-searchbar show-cancel-button="always"></ion-searchbar>
<!-- Searchbar with cancel button never shown -->
<ion-searchbar showCancelButton="never"></ion-searchbar>
<ion-searchbar show-cancel-button="never"></ion-searchbar>
<!-- Searchbar with cancel button shown on focus -->
<ion-searchbar showCancelButton="focus"></ion-searchbar>
<ion-searchbar show-cancel-button="focus"></ion-searchbar>
<!-- Searchbar with danger color -->
<ion-searchbar color="danger"></ion-searchbar>
@@ -28,7 +28,7 @@
<ion-searchbar disabled="true"></ion-searchbar>
<!-- Searchbar with a cancel button and custom cancel button text -->
<ion-searchbar showCancelButton="focus" cancelButtonText="Custom Cancel"></ion-searchbar>
<ion-searchbar show-cancel-button="focus" cancel-button-text="Custom Cancel"></ion-searchbar>
<!-- Searchbar with a custom debounce -->
<ion-searchbar debounce="500"></ion-searchbar>

View File

@@ -793,12 +793,14 @@ Type: `Promise<any>`
## CSS Custom Properties
| Name | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------- |
| `--padding-bottom` | Bottom padding of the select |
| `--padding-end` | Right padding if direction is left-to-right, and left padding if direction is right-to-left of the select |
| `--padding-start` | Left padding if direction is left-to-right, and right padding if direction is right-to-left of the select |
| `--padding-top` | Top padding of the select |
| Name | Description |
| ----------------------- | --------------------------------------------------------------------------------------------------------- |
| `--padding-bottom` | Bottom padding of the select |
| `--padding-end` | Right padding if direction is left-to-right, and left padding if direction is right-to-left of the select |
| `--padding-start` | Left padding if direction is left-to-right, and right padding if direction is right-to-left of the select |
| `--padding-top` | Top padding of the select |
| `--placeholder-color` | Color of the select placeholder text |
| `--placeholder-opacity` | Opacity of the select placeholder text |
----------------------------------------------

View File

@@ -9,7 +9,13 @@
* @prop --padding-end: Right padding if direction is left-to-right, and left padding if direction is right-to-left of the select
* @prop --padding-bottom: Bottom padding of the select
* @prop --padding-start: Left padding if direction is left-to-right, and right padding if direction is right-to-left of the select
*
* @prop --placeholder-color: Color of the select placeholder text
* @prop --placeholder-opacity: Opacity of the select placeholder text
*/
--placeholder-color: currentColor;
--placeholder-opacity: 0.33;
@include padding(var(--padding-top), var(--padding-end), var(--padding-bottom), var(--padding-start));
display: flex;
@@ -37,9 +43,9 @@
}
.select-placeholder {
color: currentColor;
color: var(--placeholder-color);
opacity: .33;
opacity: var(--placeholder-opacity);
}
button {

View File

@@ -471,10 +471,10 @@ export class Select implements ComponentInterface {
'select-disabled': disabled,
}}
>
<div class={selectTextClasses}>
<div class={selectTextClasses} part="text">
{selectText}
</div>
<div class="select-icon" role="presentation">
<div class="select-icon" role="presentation" part="icon">
<div class="select-icon-inner"></div>
</div>
<button

View File

@@ -26,7 +26,7 @@
<ion-item>
<ion-label>Gender</ion-label>
<ion-select id="gender" placeholder="Select One">
<ion-select id="gender" placeholder="Select One" class="custom-select">
<ion-select-option value="fn">Female</ion-select-option>
<ion-select-option value="ml">Male</ion-select-option>
</ion-select>
@@ -286,6 +286,9 @@
.outer-content {
--background: #f2f2f2;
}
.custom-select {
--placeholder-opacity: 0.5;
}
</style>
<script>

View File

@@ -761,10 +761,14 @@ Type: `Promise<void>`
## CSS Custom Properties
| Name | Description |
| ---------------------------- | ------------------------------------------ |
| `--bullet-background` | Background of the pagination bullets |
| `--bullet-background-active` | Background of the active pagination bullet |
| Name | Description |
| ---------------------------------- | ------------------------------------------------ |
| `--bullet-background` | Background of the pagination bullets |
| `--bullet-background-active` | Background of the active pagination bullet |
| `--progress-bar-background` | Background of the pagination progress-bar |
| `--progress-bar-background-active` | Background of the active pagination progress-bar |
| `--scroll-bar-background` | Background of the pagination scroll-bar |
| `--scroll-bar-background-active` | Background of the active pagination scroll-bar |
----------------------------------------------

View File

@@ -8,6 +8,12 @@ ion-slides {
/**
* @prop --bullet-background: Background of the pagination bullets
* @prop --bullet-background-active: Background of the active pagination bullet
*
* @prop --progress-bar-background: Background of the pagination progress-bar
* @prop --progress-bar-background-active: Background of the active pagination progress-bar
*
* @prop --scroll-bar-background: Background of the pagination scroll-bar
* @prop --scroll-bar-background-active: Background of the active pagination scroll-bar
*/
display: block;

View File

@@ -115,17 +115,17 @@ export const ToggleExample: React.FC = () => (
<IonList>
<IonItem>
<IonLabel>Pepperoni</IonLabel>
<IonToggle value="pepperoni" onChange={() => {}} />
<IonToggle value="pepperoni" onIonChange={() => {}} />
</IonItem>
<IonItem>
<IonLabel>Sausage</IonLabel>
<IonToggle value="sausage" onChange={() => {}} disabled={true} />
<IonToggle value="sausage" onIonChange={() => {}} disabled={true} />
</IonItem>
<IonItem>
<IonLabel>Mushrooms</IonLabel>
<IonToggle value="mushrooms" onChange={() => {}} />
<IonToggle value="mushrooms" onIonChange={() => {}} />
</IonItem>
</IonList>
</IonContent>

View File

@@ -24,17 +24,17 @@ export const ToggleExample: React.FC = () => (
<IonList>
<IonItem>
<IonLabel>Pepperoni</IonLabel>
<IonToggle value="pepperoni" onChange={() => {}} />
<IonToggle value="pepperoni" onIonChange={() => {}} />
</IonItem>
<IonItem>
<IonLabel>Sausage</IonLabel>
<IonToggle value="sausage" onChange={() => {}} disabled={true} />
<IonToggle value="sausage" onIonChange={() => {}} disabled={true} />
</IonItem>
<IonItem>
<IonLabel>Mushrooms</IonLabel>
<IonToggle value="mushrooms" onChange={() => {}} />
<IonToggle value="mushrooms" onIonChange={() => {}} />
</IonItem>
</IonList>
</IonContent>

View File

@@ -3,6 +3,8 @@ import 'ionicons';
export { createAnimation } from './utils/animation/animation';
export { getTimeGivenProgression } from './utils/animation/cubic-bezier';
export { createGesture } from './utils/gesture';
export { createPressRecognizer } from './utils/gesture/recognizers/press';
export { isPlatform, Platforms, getPlatforms } from './utils/platform';
export * from './utils/config';

View File

@@ -34,9 +34,9 @@ export * from './components/virtual-scroll/virtual-scroll-interface';
export { Animation, AnimationBuilder } from './utils/animation/animation-interface';
export * from './utils/overlays-interface';
export * from './global/config';
export { Gesture, GestureDetail } from './utils/gesture';
export { Gesture, GestureConfig, GestureDetail } from './utils/gesture';
export { PressRecognizerOptions } from './utils/gesture/recognizers/press';
// Global aux types
export type TextFieldTypes = 'date' | 'email' | 'number' | 'password' | 'search' | 'tel' | 'text' | 'url' | 'time';
export type Side = 'start' | 'end';
export type PredefinedColors = 'primary' | 'secondary' | 'tertiary' | 'success' | 'warning' | 'danger' | 'light' | 'medium' | 'dark';

View File

@@ -385,6 +385,18 @@ describe('cubic-bezier conversion', () => {
shouldApproximatelyEqual(getTimeGivenProgression(...equation, 1.02), [0.35, 0.87]);
})
it('cubic-bezier(0.32, 0.72, 0, 1) (with out of bounds progression)', () => {
const equation = [
[0, 0],
[0.05, 0.2],
[.14, 1.72],
[1, 1]
];
expect(getTimeGivenProgression(...equation, 1.32)).toEqual([]);
expect(getTimeGivenProgression(...equation, -0.32)).toEqual([]);
})
})
});

View File

@@ -203,8 +203,12 @@ export const createGesture = (config: GestureConfig): Gesture => {
return {
enable(enable = true) {
if (!enable && hasCapturedPan) {
pointerUp(undefined);
if (!enable) {
if (hasCapturedPan) {
pointerUp(undefined);
}
reset();
}
pointerEvents.enable(enable);
},

View File

@@ -0,0 +1,91 @@
import { GestureConfig, GestureDetail, createGesture } from '../index';
export interface PressRecognizerOptions extends GestureConfig {
/**
* How long the user must press the
* element before onPressHandler is fired.
*/
time: number;
/**
* The maximum amount of movement on the x and y
* axes that is allowed and still have onPressHandler fire.
*/
maxThreshold: number;
/**
* Fired when the long press occurs.
*/
onPressHandler?: () => void;
/**
* Fired when the pointer is lifted.
*/
onPressUpHandler?: () => void;
}
export const createPressRecognizer = (opts: PressRecognizerOptions) => {
const THRESHOLD = opts.maxThreshold || 10;
let timeout: any;
let pressed = false;
const onStart = (detail: GestureDetail) => {
if (opts.onStart) {
opts.onStart(detail);
}
clearGestureTimeout();
timeout = setTimeout(() => {
if (opts.onPressHandler) {
opts.onPressHandler();
}
pressed = true;
clearGestureTimeout();
}, opts.time || 500);
};
const onMove = (detail: GestureDetail) => {
if (opts.onMove) {
opts.onMove(detail);
}
if (Math.abs(detail.deltaX) + Math.abs(detail.deltaY) <= THRESHOLD) {
return;
}
clearGestureTimeout();
};
const onEnd = (detail: GestureDetail) => {
if (opts.onEnd) {
opts.onEnd(detail);
}
if (!pressed) { return; }
if (opts.onPressUpHandler) {
opts.onPressUpHandler();
}
pressed = false;
clearGestureTimeout();
};
const clearGestureTimeout = () => {
if (timeout) {
clearTimeout(timeout);
timeout = undefined;
}
};
return createGesture({
...opts,
gestureName: opts.gestureName || 'press',
threshold: opts.threshold || 0,
onStart,
onMove,
onEnd,
});
};

View File

@@ -0,0 +1,77 @@
import { newE2EPage } from '@stencil/core/testing';
test(`gesture: press`, async () => {
const page = await newE2EPage({ url: '/src/utils/gesture/recognizers/tests/press' });
const screenshotCompares = [];
screenshotCompares.push(await page.compareScreenshot());
const square = await page.$('.square');
const { x, y } = await coords(square);
page.mouse.move(x, y);
page.mouse.down();
await page.waitFor(300);
expect(await page.find('.square')).toHaveClass('pressed');
page.mouse.up();
expect(await page.find('.square')).toHaveClass('pressedUp');
screenshotCompares.push(await page.compareScreenshot('end press'));
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
test(`gesture: press:short press`, async () => {
const page = await newE2EPage({ url: '/src/utils/gesture/recognizers/tests/press' });
const screenshotCompares = [];
screenshotCompares.push(await page.compareScreenshot());
const square = await page.$('.square');
const { x, y } = await coords(square);
page.mouse.move(x, y);
page.mouse.down();
await page.waitFor(50);
page.mouse.up();
const squareAgain = await page.find('.square');
expect(squareAgain).not.toHaveClass('pressed');
expect(squareAgain).not.toHaveClass('pressedUp');
screenshotCompares.push(await page.compareScreenshot('end press'));
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
test(`gesture: press:click`, async () => {
const page = await newE2EPage({ url: '/src/utils/gesture/recognizers/tests/press' });
const screenshotCompares = [];
screenshotCompares.push(await page.compareScreenshot());
await page.click('.square');
const square = await page.find('.square');
expect(square).not.toHaveClass('pressed');
expect(square).not.toHaveClass('pressedUp');
screenshotCompares.push(await page.compareScreenshot('end press'));
for (const screenshotCompare of screenshotCompares) {
expect(screenshotCompare).toMatchScreenshot();
}
});
const coords = async el => {
const box = await el.boundingBox();
return {
x: Math.floor(box.x + (box.width / 2)),
y: Math.floor(box.y + (box.height / 2))
};
};

View File

@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Gesture - Press</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>
<script type="module">
import { createPressRecognizer } from '../../../../../dist/ionic/index.esm.js';
const square = document.querySelector('.square');
const pressRecognizer = createPressRecognizer({
el: square,
time: 251,
minThreshold: 10,
onStart: () => square.classList.remove('pressedUp'),
onPressHandler: () => square.classList.toggle('pressed'),
onPressUpHandler: () => square.classList.add('pressedUp')
});
pressRecognizer.enable(true);
</script>
<style>
.square {
width: 100px;
height: 100px;
background: rgba(255, 0, 0, 0.5);
text-align: center;
line-height: 100px;
margin-left: 25px;
margin-top: 25px;
transition: 0.2s all ease-in-out;
cursor: pointer;
}
.pressed {
transform: scale(1.5);
background: rgba(0, 255, 0, 0.5);
}
</style>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Gesture - Press</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<div class="ion-padding">
<div class="square">Hello</div>
</div>
</ion-content>
</ion-app>
</body>
</html>

View File

@@ -4,24 +4,16 @@ import { Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { RouteComponentProps } from 'react-router-dom';
import { generateId } from '../utils';
import { LocationHistory } from '../utils/LocationHistory';
import { StackManager } from './StackManager';
import { ViewItem } from './ViewItem';
import { ViewStack } from './ViewStacks';
interface NavManagerProps extends RouteComponentProps {
findViewInfoByLocation: (location: HistoryLocation) => { view?: ViewItem, viewStack?: ViewStack };
findViewInfoById: (id: string) => { view?: ViewItem, viewStack?: ViewStack };
getActiveIonPage: () => { view?: ViewItem, viewStack?: ViewStack };
onNavigateBack: (defaultHref?: string) => void;
onNavigate: (type: 'push' | 'replace', path: string, state?: any) => void;
}
export class NavManager extends React.Component<NavManagerProps, NavContextState> {
listenUnregisterCallback: UnregisterCallback | undefined;
locationHistory: LocationHistory = new LocationHistory();
constructor(props: NavManagerProps) {
super(props);
@@ -40,16 +32,15 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
this.setState({
currentPath: location.pathname
});
this.locationHistory.add(location);
});
this.locationHistory.add({
hash: window.location.hash,
key: generateId(),
pathname: window.location.pathname,
search: window.location.search,
state: {}
});
if (document) {
document.addEventListener('ionBackButton', (e: any) => {
e.detail.register(0, () => {
this.props.history.goBack();
});
});
}
}
componentWillUnmount() {
@@ -59,26 +50,7 @@ export class NavManager extends React.Component<NavManagerProps, NavContextState
}
goBack(defaultHref?: string) {
const { view: activeIonPage } = this.props.getActiveIonPage();
if (activeIonPage) {
const { view: enteringView } = this.props.findViewInfoById(activeIonPage.prevId!);
if (enteringView) {
const lastLocation = this.locationHistory.findLastLocation(enteringView.routeData.match.url);
if (lastLocation) {
this.props.onNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
} else {
this.props.onNavigate('replace', enteringView.routeData.match.url, 'back');
}
} else {
if (defaultHref) {
this.props.onNavigate('replace', defaultHref, 'back');
}
}
} else {
if (defaultHref) {
this.props.onNavigate('replace', defaultHref, 'back');
}
}
this.props.onNavigateBack(defaultHref);
}
navigate(path: string, direction?: RouterDirection | 'none') {

View File

@@ -6,7 +6,7 @@ export interface RouteManagerContextState {
syncView: (page: HTMLElement, viewId: string) => void;
hideView: (viewId: string) => void;
viewStacks: ViewStacks;
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => Promise<void>;
setupIonRouter: (id: string, children: ReactNode, routerOutlet: HTMLIonRouterOutletElement) => void;
removeViewStack: (stack: string) => void;
}

View File

@@ -1,10 +1,11 @@
import { NavDirection } from '@ionic/core';
import { RouterDirection } from '@ionic/react';
import { RouterDirection, getConfig } from '@ionic/react';
import { Action as HistoryAction, Location as HistoryLocation, UnregisterCallback } from 'history';
import React from 'react';
import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
import { generateId } from '../utils';
import { generateId, isDevMode } from '../utils';
import { LocationHistory } from '../utils/LocationHistory';
import { IonRouteData } from './IonRouteData';
import { NavManager } from './NavManager';
@@ -21,11 +22,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
listenUnregisterCallback: UnregisterCallback | undefined;
activeIonPageId?: string;
currentDirection?: RouterDirection;
locationHistory = new LocationHistory();
constructor(props: RouteComponentProps) {
super(props);
this.listenUnregisterCallback = this.props.history.listen(this.historyChange.bind(this));
this.handleNavigate = this.handleNavigate.bind(this);
this.navigateBack = this.navigateBack.bind(this);
this.state = {
viewStacks: new ViewStacks(),
hideView: this.hideView.bind(this),
@@ -33,6 +36,14 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
removeViewStack: this.removeViewStack.bind(this),
syncView: this.syncView.bind(this)
};
this.locationHistory.add({
hash: window.location.hash,
key: generateId(),
pathname: window.location.pathname,
search: window.location.search,
state: {}
});
}
componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
@@ -66,6 +77,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
historyChange(location: HistoryLocation, action: HistoryAction) {
location.state = location.state || { direction: this.currentDirection };
this.currentDirection = undefined;
if (action === 'PUSH') {
this.locationHistory.add(location);
} else if ((action === 'REPLACE' && location.state.direction === 'back') || action === 'POP') {
this.locationHistory.pop();
} else {
this.locationHistory.replace(location);
}
this.setState({
location,
action
@@ -99,21 +117,22 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
* If the page is being pushed into the stack by another view,
* record the view that originally directed to the new view for back button purposes.
*/
enteringView.prevId = enteringView.prevId || leavingView.id;
enteringView.prevId = leavingView.id;
} else if (action === 'POP') {
direction = leavingView.prevId === enteringView.id ? 'back' : 'none';
} else {
direction = direction || 'back';
leavingView.mount = false;
}
} else if (direction === 'back' || action === 'REPLACE') {
}
if (direction === 'back' || action === 'REPLACE') {
leavingView.mount = false;
this.removeOrphanedViews(enteringView, enteringViewStack);
}
} else {
// If there is not a leavingView, then we shouldn't provide a direction
direction = undefined;
}
this.removeOrphanedViews(enteringView, enteringViewStack);
} else {
enteringView.show = true;
enteringView.mount = true;
@@ -139,21 +158,36 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
if (enteringEl) {
// Don't animate from an empty view
const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction;
this.transitionView(
const shouldGoBack = !!enteringView.prevId || !!this.locationHistory.previous();
this.commitView(
enteringEl!,
leavingEl!,
viewStack.routerOutlet,
navDirection,
!!enteringView.prevId);
shouldGoBack);
} else if (leavingEl) {
leavingEl.classList.add('ion-page-hidden');
leavingEl.setAttribute('aria-hidden', 'true');
}
// Warn if an IonPage was not eventually rendered in Dev Mode
if (isDevMode()) {
if (enteringView.routeData.match!.url !== location.pathname) {
setTimeout(() => {
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (view!.routeData.match!.url !== location.pathname) {
console.warn('No IonPage was found to render. Make sure you wrap your page with an IonPage component.');
}
}, 100);
}
}
}
});
}
removeOrphanedViews(view: ViewItem, viewStack: ViewStack) {
// Note: This technique is a bit wonky for views that reference each other and get into a circular loop.
// It can still remove a view that probably shouldn't be.
const viewsToRemove = viewStack.views.filter(v => v.prevId === view.id);
viewsToRemove.forEach(v => {
// Don't remove if view is currently active
@@ -173,7 +207,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
});
}
async setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
setupIonRouter(id: string, children: any, routerOutlet: HTMLIonRouterOutletElement) {
const views: ViewItem[] = [];
let activeId: string | undefined;
const ionRouterOutlet = React.Children.only(children) as React.ReactElement;
@@ -181,7 +215,7 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
views.push(createViewItem(child, this.props.history.location));
});
await this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
this.registerViewStack(id, activeId, views, routerOutlet, this.props.location);
function createViewItem(child: React.ReactElement<any>, location: HistoryLocation) {
const viewId = generateId();
@@ -212,29 +246,61 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
}
async registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, _location: HistoryLocation) {
return new Promise(resolve => {
this.setState(prevState => {
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
const newStack: ViewStack = {
id: stack,
views: stackItems,
routerOutlet
};
if (activeId) {
this.activeIonPageId = activeId;
}
prevViewStacks.set(stack, newStack);
return {
viewStacks: prevViewStacks
};
}, () => {
resolve();
});
registerViewStack(stack: string, activeId: string | undefined, stackItems: ViewItem[], routerOutlet: HTMLIonRouterOutletElement, _location: HistoryLocation) {
this.setState(prevState => {
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
const newStack: ViewStack = {
id: stack,
views: stackItems,
routerOutlet
};
if (activeId) {
this.activeIonPageId = activeId;
}
prevViewStacks.set(stack, newStack);
return {
viewStacks: prevViewStacks
};
}, () => {
this.setupRouterOutlet(routerOutlet);
});
}
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
const waitUntilReady = async () => {
if (routerOutlet.componentOnReady) {
routerOutlet.dispatchEvent(new Event('routerOutletReady'));
return;
} else {
setTimeout(() => {
waitUntilReady();
}, 0);
}
};
await waitUntilReady();
const canStart = () => {
const config = getConfig();
const swipeEnabled = config && config.get('swipeBackEnabled', routerOutlet.mode === 'ios');
if (swipeEnabled) {
const { view } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
return !!(view && view.prevId);
} else {
return false;
}
};
const onStart = () => {
this.navigateBack();
};
routerOutlet.swipeHandler = {
canStart,
onStart,
onEnd: _shouldContinue => true
};
}
removeViewStack(stack: string) {
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
viewStacks.delete(stack);
@@ -245,7 +311,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
syncView(page: HTMLElement, viewId: string) {
this.setState(state => {
const viewStacks = Object.assign(new ViewStacks(), state.viewStacks);
const { view } = viewStacks.findViewInfoById(viewId);
@@ -261,20 +326,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
});
}
transitionView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement | undefined, direction: NavDirection | undefined, showGoBack: boolean) {
/**
* Super hacky workaround to make sure ionRouterOutlet is available
* since transitionView might be called before IonRouterOutlet is fully mounted
*/
if (ionRouterOutlet && ionRouterOutlet.componentOnReady) {
this.commitView(enteringEl, leavingEl, ionRouterOutlet, direction, showGoBack);
} else {
setTimeout(() => {
this.transitionView(enteringEl, leavingEl, ionRouterOutlet, direction, showGoBack);
}, 10);
}
}
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOuter: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean) {
if (enteringEl === leavingEl) {
@@ -305,15 +356,41 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
}
}
navigateBack(defaultHref?: string) {
const { view: activeIonPage } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
if (activeIonPage) {
const { view: enteringView } = this.state.viewStacks.findViewInfoById(activeIonPage.prevId);
if (enteringView) {
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
if (lastLocation) {
this.handleNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
} else {
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
}
} else {
const currentLocation = this.locationHistory.previous();
if (currentLocation) {
this.handleNavigate('replace', currentLocation.pathname + currentLocation.search, 'back');
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
}
}
}
} else {
if (defaultHref) {
this.handleNavigate('replace', defaultHref, 'back');
}
}
}
render() {
return (
<RouteManagerContext.Provider value={this.state}>
<NavManager
{...this.props}
onNavigateBack={this.navigateBack}
onNavigate={this.handleNavigate}
findViewInfoById={(id: string) => this.state.viewStacks.findViewInfoById(id)}
findViewInfoByLocation={(location: HistoryLocation) => this.state.viewStacks.findViewInfoByLocation(location)}
getActiveIonPage={() => this.state.viewStacks.findViewInfoById(this.activeIonPageId)}
>
{this.props.children}
</NavManager>

View File

@@ -11,7 +11,11 @@ interface StackManagerProps {
id?: string;
}
export class StackManager extends React.Component<StackManagerProps, {}> {
interface StackManagerState {
routerOutletReady: boolean;
}
export class StackManager extends React.Component<StackManagerProps, StackManagerState> {
routerOutletEl: React.RefObject<HTMLIonRouterOutletElement> = React.createRef();
context!: React.ContextType<typeof RouteManagerContext>;
id: string;
@@ -21,10 +25,18 @@ export class StackManager extends React.Component<StackManagerProps, {}> {
this.id = this.props.id || generateId();
this.handleViewSync = this.handleViewSync.bind(this);
this.handleHideView = this.handleHideView.bind(this);
this.state = {
routerOutletReady: false
};
}
componentDidMount() {
this.context.setupIonRouter(this.id, this.props.children, this.routerOutletEl.current!);
this.routerOutletEl.current!.addEventListener('routerOutletReady', () => {
this.setState({
routerOutletReady: true
});
});
}
componentWillUnmount() {
@@ -51,8 +63,9 @@ export class StackManager extends React.Component<StackManagerProps, {}> {
const viewStack = context.viewStacks.get(this.id);
const views = (viewStack || { views: [] }).views.filter(x => x.show);
const ionRouterOutlet = React.Children.only(this.props.children) as React.ReactElement;
const { routerOutletReady } = this.state;
const childElements = views.map(view => {
const childElements = routerOutletReady ? views.map(view => {
return (
<ViewTransitionManager
id={view.id}
@@ -68,7 +81,7 @@ export class StackManager extends React.Component<StackManagerProps, {}> {
</View>
</ViewTransitionManager>
);
});
}) : <div></div>;
const elementProps: any = {
ref: this.routerOutletEl

View File

@@ -12,9 +12,30 @@ export class LocationHistory {
}
}
findLastLocation(url: string) {
const reversedLocations = [...this.locationHistory].reverse();
const last = reversedLocations.find(x => x.pathname.toLowerCase() === url.toLowerCase());
return last;
pop() {
this.locationHistory.pop();
}
replace(location: HistoryLocation) {
this.locationHistory.pop();
this.locationHistory.push(location);
}
findLastLocationByUrl(url: string) {
for (let i = this.locationHistory.length - 1; i >= 0; i--) {
const location = this.locationHistory[i];
if (location.pathname.toLocaleLowerCase() === url.toLocaleLowerCase()) {
return location;
}
}
return undefined;
}
previous() {
return this.locationHistory[this.locationHistory.length - 2];
}
current() {
return this.locationHistory[this.locationHistory.length - 1];
}
}

View File

@@ -19,7 +19,7 @@
"no-invalid-template-strings": true,
"ban-export-const-enum": true,
"only-arrow-functions": false,
"strict-boolean-conditions": [true, "allow-null-union", "allow-undefined-union", "allow-boolean-or-undefined", "allow-string"],
"strict-boolean-conditions": [false],
"jsx-key": false,
"jsx-self-close": false,
"jsx-curly-spacing": [true, "never"],

View File

@@ -23,6 +23,7 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
return class extends React.Component<Props> {
overlay?: OverlayType;
isUnmounted = false;
constructor(props: Props) {
super(props);
@@ -40,6 +41,7 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
}
componentWillUnmount() {
this.isUnmounted = true;
if (this.overlay) { this.overlay.dismiss(); }
}
@@ -54,18 +56,16 @@ export const createControllerComponent = <OptionsType extends object, OverlayTyp
async present(prevProps?: Props) {
const { isOpen, onDidDismiss, ...cProps } = this.props;
let overlay = this.overlay;
if (!overlay) {
overlay = this.overlay = await controller.create({
...cProps as any
});
}
attachProps(overlay, {
this.overlay = await controller.create({
...cProps as any
});
attachProps(this.overlay, {
[dismissEventName]: onDidDismiss
}, prevProps);
// Check isOpen again since the value could of changed during the async call to controller.create
if (this.props.isOpen === true) {
await overlay.present();
// Check isOpen again since the value could have changed during the async call to controller.create
// It's also possible for the component to have become unmounted.
if (this.props.isOpen === true && this.isUnmounted === false) {
await this.overlay.present();
}
}

View File

@@ -24,7 +24,7 @@ export { IonBackButton } from './navigation/IonBackButton';
export { IonRouterOutlet } from './IonRouterOutlet';
// Utils
export { isPlatform, getPlatforms } from './utils';
export { isPlatform, getPlatforms, getConfig } from './utils';
export { RouterDirection } from './hrefprops';
// Icons that are used by internal components

View File

@@ -1,4 +1,4 @@
import { Platforms, getPlatforms as getPlatformsCore, isPlatform as isPlatformCore } from '@ionic/core';
import { Config as CoreConfig, Platforms, getPlatforms as getPlatformsCore, isPlatform as isPlatformCore } from '@ionic/core';
import React from 'react';
import { IonicReactProps } from '../IonicReactProps';
@@ -24,3 +24,13 @@ export const isPlatform = (platform: Platforms) => {
export const getPlatforms = () => {
return getPlatformsCore(window);
};
export const getConfig = (): CoreConfig | null => {
if (typeof (window as any) !== 'undefined') {
const Ionic = (window as any).Ionic;
if (Ionic && Ionic.config) {
return Ionic.config;
}
}
return null;
};

View File

@@ -22,52 +22,46 @@ export const IonLifeCycleContext = /*@__PURE__*/React.createContext<IonLifeCycle
ionViewDidLeave: () => { return; },
});
type LifeCycleCallback = () => void;
export const DefaultIonLifeCycleContext = class implements IonLifeCycleContextInterface {
ionViewWillEnterCallback?: () => void;
ionViewDidEnterCallback?: () => void;
ionViewWillLeaveCallback?: () => void;
ionViewDidLeaveCallback?: () => void;
ionViewWillEnterCallbacks: LifeCycleCallback[] = [];
ionViewDidEnterCallbacks: LifeCycleCallback[] = [];
ionViewWillLeaveCallbacks: LifeCycleCallback[] = [];
ionViewDidLeaveCallbacks: LifeCycleCallback[] = [];
componentCanBeDestroyedCallback?: () => void;
onIonViewWillEnter(callback: () => void) {
this.ionViewWillEnterCallback = callback;
onIonViewWillEnter(callback: LifeCycleCallback) {
this.ionViewWillEnterCallbacks.push(callback);
}
ionViewWillEnter() {
if (this.ionViewWillEnterCallback) {
this.ionViewWillEnterCallback();
}
this.ionViewWillEnterCallbacks.forEach(cb => cb());
}
onIonViewDidEnter(callback: () => void) {
this.ionViewDidEnterCallback = callback;
onIonViewDidEnter(callback: LifeCycleCallback) {
this.ionViewDidEnterCallbacks.push(callback);
}
ionViewDidEnter() {
if (this.ionViewDidEnterCallback) {
this.ionViewDidEnterCallback();
}
this.ionViewDidEnterCallbacks.forEach(cb => cb());
}
onIonViewWillLeave(callback: () => void) {
this.ionViewWillLeaveCallback = callback;
onIonViewWillLeave(callback: LifeCycleCallback) {
this.ionViewWillLeaveCallbacks.push(callback);
}
ionViewWillLeave() {
if (this.ionViewWillLeaveCallback) {
this.ionViewWillLeaveCallback();
}
this.ionViewWillLeaveCallbacks.forEach(cb => cb());
}
onIonViewDidLeave(callback: () => void) {
this.ionViewDidLeaveCallback = callback;
onIonViewDidLeave(callback: LifeCycleCallback) {
this.ionViewDidLeaveCallbacks.push(callback);
}
ionViewDidLeave() {
if (this.ionViewDidLeaveCallback) {
this.ionViewDidLeaveCallback();
}
this.ionViewDidLeaveCallbacks.forEach(cb => cb());
this.componentCanBeDestroyed();
}

View File

@@ -1,23 +1,31 @@
import { useContext } from 'react';
import { useContext, useEffect } from 'react';
import { IonLifeCycleContext } from '../contexts/IonLifeCycleContext';
export const useIonViewWillEnter = (callback: () => void) => {
const value = useContext(IonLifeCycleContext);
value.onIonViewWillEnter(callback);
const context = useContext(IonLifeCycleContext);
useEffect(() => {
context.onIonViewWillEnter(callback);
}, []);
};
export const useIonViewDidEnter = (callback: () => void) => {
const value = useContext(IonLifeCycleContext);
value.onIonViewDidEnter(callback);
const context = useContext(IonLifeCycleContext);
useEffect(() => {
context.onIonViewDidEnter(callback);
}, []);
};
export const useIonViewWillLeave = (callback: () => void) => {
const value = useContext(IonLifeCycleContext);
value.onIonViewWillLeave(callback);
const context = useContext(IonLifeCycleContext);
useEffect(() => {
context.onIonViewWillLeave(callback);
}, []);
};
export const useIonViewDidLeave = (callback: () => void) => {
const value = useContext(IonLifeCycleContext);
value.onIonViewDidLeave(callback);
const context = useContext(IonLifeCycleContext);
useEffect(() => {
context.onIonViewDidLeave(callback);
}, []);
};