merge release-4.6.1

This commit is contained in:
Brandy Carney
2019-07-09 12:41:23 -04:00
committed by GitHub
45 changed files with 588 additions and 269 deletions

View File

@ -2016,6 +2016,10 @@ export namespace Components {
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection': RouterDirection;
/**
* Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`.
*/
'target': string | undefined;
}
interface IonRouterOutlet {
/**
@ -2029,6 +2033,10 @@ export namespace Components {
'commit': (enteringEl: HTMLElement, leavingEl: HTMLElement | undefined, opts?: RouterOutletOptions | undefined) => Promise<boolean>;
'delegate'?: FrameworkDelegate;
'getRouteId': () => Promise<RouteID | undefined>;
/**
* The mode determines which platform styles to use.
*/
'mode': "ios" | "md";
'setRouteId': (id: string, params: ComponentProps<null> | undefined, direction: RouterDirection) => Promise<RouteWrite>;
'swipeHandler'?: SwipeGestureHandler;
}
@ -5244,6 +5252,10 @@ declare namespace LocalJSX {
* When using a router, it specifies the transition direction when navigating to another page using `href`.
*/
'routerDirection'?: RouterDirection;
/**
* Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`.
*/
'target'?: string | undefined;
}
interface IonRouterOutlet extends JSXBase.HTMLAttributes<HTMLIonRouterOutletElement> {
/**
@ -5254,6 +5266,10 @@ declare namespace LocalJSX {
* By default `ion-nav` animates transition between pages based in the mode (ios or material design). However, this property allows to create custom transition using `AnimateBuilder` functions.
*/
'animation'?: AnimationBuilder;
/**
* The mode determines which platform styles to use.
*/
'mode'?: "ios" | "md";
}
interface IonRow extends JSXBase.HTMLAttributes<HTMLIonRowElement> {}
interface IonSearchbar extends JSXBase.HTMLAttributes<HTMLIonSearchbarElement> {

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me
import { getIonMode } from '../../global/ionic-global';
import { ActionSheetButton, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { BACKDROP, dismiss, eventMethod, isCancel, present } from '../../utils/overlays';
import { BACKDROP, dismiss, eventMethod, isCancel, present, safeCall } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme';
import { iosEnterAnimation } from './animations/ios.enter';
@ -169,17 +169,13 @@ export class ActionSheet implements ComponentInterface, OverlayInterface {
}
private async callButtonHandler(button: ActionSheetButton | undefined) {
if (button && button.handler) {
if (button) {
// a handler has been provided, execute it
// pass the handler the values from the inputs
try {
const rtn = await button.handler();
if (rtn === false) {
// if the return value of the handler is false then do not dismiss
return false;
}
} catch (e) {
console.error(e);
const rtn = await safeCall(button.handler);
if (rtn === false) {
// if the return value of the handler is false then do not dismiss
return false;
}
}
return true;

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me
import { getIonMode } from '../../global/ionic-global';
import { AlertButton, AlertInput, Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface } from '../../interface';
import { BACKDROP, dismiss, eventMethod, isCancel, present } from '../../utils/overlays';
import { BACKDROP, dismiss, eventMethod, isCancel, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { getClassMap } from '../../utils/theme';
@ -223,17 +223,13 @@ export class Alert implements ComponentInterface, OverlayInterface {
input.checked = input === selectedInput;
}
this.activeId = selectedInput.id;
if (selectedInput.handler) {
selectedInput.handler(selectedInput);
}
safeCall(selectedInput.handler, selectedInput);
this.el.forceUpdate();
}
private cbClick(selectedInput: AlertInput) {
selectedInput.checked = !selectedInput.checked;
if (selectedInput.handler) {
selectedInput.handler(selectedInput);
}
safeCall(selectedInput.handler, selectedInput);
this.el.forceUpdate();
}
@ -254,7 +250,7 @@ export class Alert implements ComponentInterface, OverlayInterface {
if (button && button.handler) {
// a handler has been provided, execute it
// pass the handler the values from the inputs
const returnData = button.handler(data);
const returnData = safeCall(button.handler, data);
if (returnData === false) {
// if the return value of the handler is false then do not dismiss
return false;

View File

@ -132,7 +132,7 @@
// --------------------------------------------------
:host(.fab-button-disabled) {
--opacity: .5;
opacity: .5;
pointer-events: none;
}

View File

@ -29,7 +29,8 @@ export class InfiniteScroll implements ComponentInterface {
@Prop() threshold = '15%';
@Watch('threshold')
protected thresholdChanged(val: string) {
protected thresholdChanged() {
const val = this.threshold;
if (val.lastIndexOf('%') > -1) {
this.thrPx = 0;
this.thrPc = (parseFloat(val) / 100);
@ -53,10 +54,12 @@ export class InfiniteScroll implements ComponentInterface {
@Watch('disabled')
protected disabledChanged() {
if (this.disabled) {
const disabled = this.disabled;
if (disabled) {
this.isLoading = false;
this.isBusy = false;
}
this.enableScrollEvents(!disabled);
}
/**
@ -79,7 +82,8 @@ export class InfiniteScroll implements ComponentInterface {
await contentEl.componentOnReady();
this.scrollEl = await contentEl.getScrollElement();
}
this.thresholdChanged(this.threshold);
this.thresholdChanged();
this.disabledChanged();
if (this.position === 'top') {
writeTask(() => {
if (this.scrollEl) {
@ -90,6 +94,7 @@ export class InfiniteScroll implements ComponentInterface {
}
componentDidUnload() {
this.enableScrollEvents(false);
this.scrollEl = undefined;
}
@ -199,16 +204,26 @@ export class InfiniteScroll implements ComponentInterface {
);
}
private enableScrollEvents(shouldListen: boolean) {
if (this.scrollEl) {
if (shouldListen) {
this.scrollEl.addEventListener('scroll', this.onScroll);
} else {
this.scrollEl.removeEventListener('scroll', this.onScroll);
}
}
}
render() {
const mode = getIonMode(this);
const disabled = this.disabled;
return (
<Host
class={{
[mode]: true,
'infinite-scroll-loading': this.isLoading,
'infinite-scroll-enabled': !this.disabled
'infinite-scroll-enabled': !disabled
}}
onScroll={this.disabled ? undefined : this.onScroll}
/>
);
}

View File

@ -149,7 +149,7 @@
// Item: Disabled
// --------------------------------------------------
:host(.item-interactive-disabled) {
:host(.item-interactive-disabled:not(.item-multiple-inputs)) {
cursor: default;
pointer-events: none;
}
@ -397,7 +397,9 @@ button, a {
// Multiple inputs in an item should have the input
// cover relative to themselves instead of the item
:host(.item-multiple-inputs) ::slotted(ion-checkbox),
:host(.item-multiple-inputs) ::slotted(ion-datetime),
:host(.item-multiple-inputs) ::slotted(ion-radio),
:host(.item-multiple-inputs) ::slotted(ion-select) {
position: relative;
}

View File

@ -128,18 +128,26 @@ export class Item implements ComponentInterface, AnchorInterface, ButtonInterfac
}
componentDidLoad() {
// Check for multiple inputs to change the position to relative
const inputs = this.el.querySelectorAll('ion-select, ion-datetime');
this.multipleInputs = inputs.length > 1 ? true : false;
// The following elements have a clickable cover that is relative to the entire item
const covers = this.el.querySelectorAll('ion-checkbox, ion-datetime, ion-select, ion-radio');
// The following elements can accept focus alongside the previous elements
// therefore if these elements are also a child of item, we don't want the
// input cover on top of those interfering with their clicks
const inputs = this.el.querySelectorAll('ion-input, ion-range, ion-searchbar, ion-segment, ion-textarea, ion-toggle');
// Check for multiple inputs to change the position of the input cover to relative
// for all of the covered inputs above
this.multipleInputs = covers.length + inputs.length > 1;
}
// If the item contains an input including a radio, checkbox, datetime, etc.
// then the item will have a clickable input cover that should
// get the hover, focused and activated states UNLESS it has multiple
// inputs, then those need to individually get the click
// If the item contains an input including a checkbox, datetime, select, or radio
// then the item will have a clickable input cover that covers the item
// that should get the hover, focused and activated states UNLESS it has multiple
// inputs, then those need to individually get each click
private hasCover(): boolean {
const inputs = this.el.querySelectorAll('ion-checkbox, ion-datetime, ion-select, ion-radio');
return inputs.length > 0 && !this.multipleInputs;
return inputs.length === 1 && !this.multipleInputs;
}
// If the item has an href or button property it will render a native

View File

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

View File

@ -0,0 +1,197 @@
<!DOCTYPE html>
<html dir="ltr">
<head>
<meta charset="UTF-8">
<title>Item - Disabled</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 src="../../../../../dist/ionic.js"></script>
</head>
<body>
<ion-app>
<ion-header>
<ion-toolbar>
<ion-title>Item: Disabled</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-vertical">
<ion-list>
<ion-list-header>
<ion-label>Single Input Disabled Items</ion-label>
</ion-list-header>
<ion-item disabled>
<ion-label>Disabled Item</ion-label>
</ion-item>
<ion-item disabled button>
<ion-label>Disabled Item Button</ion-label>
</ion-item>
<ion-item disabled href="#">
<ion-label>Disabled Item Anchor</ion-label>
</ion-item>
<ion-item>
<ion-label>Disabled Datetime</ion-label>
<ion-datetime disabled value="2019"></ion-datetime>
</ion-item>
<ion-item>
<ion-label>Disabled Select</ion-label>
<ion-select disabled>
<ion-select-option value="">No Game Console</ion-select-option>
<ion-select-option value="nes">NES</ion-select-option>
<ion-select-option value="n64" selected>Nintendo64</ion-select-option>
<ion-select-option value="ps">PlayStation</ion-select-option>
<ion-select-option value="genesis">Sega Genesis</ion-select-option>
<ion-select-option value="saturn">Sega Saturn</ion-select-option>
<ion-select-option value="snes">SNES</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-label>Disabled Input</ion-label>
<ion-input placeholder="Disabled" disabled></ion-input>
</ion-item>
<ion-item>
<ion-label>Disabled Toggle</ion-label>
<ion-toggle disabled checked slot="end"></ion-toggle>
</ion-item>
<ion-item>
<ion-label>Disabled Checkbox</ion-label>
<ion-checkbox disabled checked slot="start"></ion-checkbox>
</ion-item>
<ion-item>
<ion-label>Disabled Range</ion-label>
<ion-range disabled value="10"></ion-range>
</ion-item>
</ion-list>
<ion-list>
<ion-list-header>
<ion-label>Multiple Input Disabled Items</ion-label>
</ion-list-header>
<ion-item>
<ion-checkbox disabled slot="start"></ion-checkbox>
<ion-input placeholder="Disabled Checkbox w/ Input"></ion-input>
</ion-item>
<ion-item>
<ion-checkbox disabled slot="end"></ion-checkbox>
<ion-input placeholder="Disabled Checkbox w/ Input"></ion-input>
</ion-item>
<ion-item>
<ion-radio disabled slot="start"></ion-radio>
<ion-input placeholder="Disabled Radio w/ Input"></ion-input>
</ion-item>
<ion-item>
<ion-radio disabled slot="end"></ion-radio>
<ion-input placeholder="Disabled Radio w/ Input"></ion-input>
</ion-item>
<ion-item>
<ion-checkbox disabled slot="start"></ion-checkbox>
<ion-label>Checkbox + Radio</ion-label>
<ion-radio slot="end"></ion-radio>
</ion-item>
<ion-item>
<ion-checkbox disabled slot="start"></ion-checkbox>
<ion-radio slot="start"></ion-radio>
<ion-label>Checkbox + Radio</ion-label>
</ion-item>
<ion-item>
<ion-label>Disabled Selects</ion-label>
<ion-select placeholder="month">
<ion-select-option value="1">January</ion-select-option>
<ion-select-option value="2">February</ion-select-option>
<ion-select-option value="3">March</ion-select-option>
</ion-select>
<ion-select disabled placeholder="year">
<ion-select-option value="1990">1990</ion-select-option>
<ion-select-option value="1991">1991</ion-select-option>
<ion-select-option value="1992">1992</ion-select-option>
<ion-select-option value="1993">1993</ion-select-option>
<ion-select-option value="1994">1994</ion-select-option>
<ion-select-option value="1995">1995</ion-select-option>
<ion-select-option value="1996">1996</ion-select-option>
<ion-select-option value="1997">1997</ion-select-option>
</ion-select>
</ion-item>
<ion-item>
<ion-checkbox slot="start"></ion-checkbox>
<ion-label>Checkbox + Range</ion-label>
<ion-range disabled value="45"></ion-range>
</ion-item>
<ion-item>
<ion-checkbox slot="start"></ion-checkbox>
<ion-label>Checkbox + Toggle</ion-label>
<ion-toggle disabled value="45"></ion-toggle>
</ion-item>
<ion-item>
<ion-checkbox disabled slot="start"></ion-checkbox>
<ion-label>Checkbox + Buttons</ion-label>
<ion-button slot="end">Default</ion-button>
<ion-button slot="end">Buttons</ion-button>
</ion-item>
<ion-item>
<ion-label>Disabled Input</ion-label>
<ion-input slot="start" placeholder="Disabled" disabled></ion-input>
<ion-segment>
<ion-segment-button value="friends">
<ion-label>Friends</ion-label>
</ion-segment-button>
<ion-segment-button value="enemies">
<ion-label>Enemies</ion-label>
</ion-segment-button>
</ion-segment>
</ion-item>
<ion-item>
<ion-checkbox slot="start" disabled></ion-checkbox>
<ion-label>Disabled Checkbox</ion-label>
<ion-searchbar></ion-searchbar>
</ion-item>
</ion-list>
</ion-content>
<ion-footer>
<ion-toolbar>
<ion-buttons slot="start">
<ion-button onClick="toggleDisabled()">Toggle</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-footer>
</ion-app>
<script>
const disabledEls = document.querySelectorAll("[disabled]");
function toggleDisabled() {
for (var i = 0; i < disabledEls.length; i++) {
disabledEls[i].disabled = !disabledEls[i].disabled;
}
}
</script>
</html>

View File

@ -34,7 +34,7 @@
white-space: normal;
}
:host-context(.item-interactive-disabled) {
:host-context(.item-interactive-disabled:not(.item-multiple-inputs)) {
cursor: default;
opacity: .3;
pointer-events: none;

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Listen, Me
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, CssClassMap, OverlayEventDetail, OverlayInterface, PickerButton, PickerColumn } from '../../interface';
import { dismiss, eventMethod, present } from '../../utils/overlays';
import { dismiss, eventMethod, present, safeCall } from '../../utils/overlays';
import { getClassMap } from '../../utils/theme';
import { iosEnterAnimation } from './animations/ios.enter';
@ -175,17 +175,9 @@ export class Picker implements ComponentInterface, OverlayInterface {
// }
// keep the time of the most recent button click
let shouldDismiss = true;
if (button.handler) {
// a handler has been provided, execute it
// pass the handler the values from the inputs
if (button.handler(this.getSelected()) === false) {
// if the return value of the handler is false then do not dismiss
shouldDismiss = false;
}
}
// a handler has been provided, execute it
// pass the handler the values from the inputs
const shouldDismiss = safeCall(button.handler, this.getSelected()) !== false;
if (shouldDismiss) {
return this.dismiss();
}

View File

@ -15,6 +15,7 @@ The router link component is used for navigating to a specified link. Similar to
| `href` | `href` | Contains a URL or a URL fragment that the hyperlink points to. If this property is set, an anchor tag will be rendered. | `string \| undefined` | `undefined` |
| `rel` | `rel` | Specifies the relationship of the target object to the link object. The value is a space-separated list of [link types](https://developer.mozilla.org/en-US/docs/Web/HTML/Link_types). | `string \| undefined` | `undefined` |
| `routerDirection` | `router-direction` | When using a router, it specifies the transition direction when navigating to another page using `href`. | `"back" \| "forward" \| "root"` | `'forward'` |
| `target` | `target` | Specifies where to display the linked URL. Only applies when an `href` is provided. Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`. | `string \| undefined` | `undefined` |
## CSS Custom Properties

View File

@ -36,6 +36,13 @@ export class RouterLink implements ComponentInterface {
*/
@Prop() routerDirection: RouterDirection = 'forward';
/**
* Specifies where to display the linked URL.
* Only applies when an `href` is provided.
* Special keywords: `"_blank"`, `"_self"`, `"_parent"`, `"_top"`.
*/
@Prop() target: string | undefined;
private onClick = (ev: Event) => {
openURL(this.href, ev, this.routerDirection);
}
@ -44,7 +51,8 @@ export class RouterLink implements ComponentInterface {
const mode = getIonMode(this);
const attrs = {
href: this.href,
rel: this.rel
rel: this.rel,
target: this.target
};
return (
<Host

View File

@ -30,7 +30,7 @@
</p>
<p>
<ion-router-link href="https://ionicframework.com" rel="external" style="text-decoration: underline">Underline Router Link</ion-router-link>
<ion-router-link href="https://ionicframework.com" rel="external" target="_blank" style="text-decoration: underline">External Router Link</ion-router-link>
</p>
<p>

View File

@ -30,10 +30,11 @@ For handling Router Guards, the older `ionViewCanEnter` and `ionViewCanLeave` ha
## Properties
| Property | Attribute | Description | Type | Default |
| ----------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ----------- |
| `animated` | `animated` | If `true`, the router-outlet should animate the transition of components. | `boolean` | `true` |
| `animation` | -- | By default `ion-nav` animates transition between pages based in the mode (ios or material design). However, this property allows to create custom transition using `AnimateBuilder` functions. | `((Animation: Animation, baseEl: any, opts?: any) => Promise<Animation>) \| undefined` | `undefined` |
| Property | Attribute | Description | Type | Default |
| ----------- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------ |
| `animated` | `animated` | If `true`, the router-outlet should animate the transition of components. | `boolean` | `true` |
| `animation` | -- | By default `ion-nav` animates transition between pages based in the mode (ios or material design). However, this property allows to create custom transition using `AnimateBuilder` functions. | `((Animation: Animation, baseEl: any, opts?: any) => Promise<Animation>) \| undefined` | `undefined` |
| `mode` | `mode` | The mode determines which platform styles to use. | `"ios" \| "md"` | `getIonMode(this)` |
----------------------------------------------

View File

@ -21,6 +21,11 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
@Element() el!: HTMLElement;
/**
* The mode determines which platform styles to use.
*/
@Prop({ mutable: true }) mode = getIonMode(this);
/** @internal */
@Prop() delegate?: FrameworkDelegate;
@ -147,8 +152,7 @@ export class RouterOutlet implements ComponentInterface, NavOutlet {
// emit nav will change event
this.ionNavWillChange.emit();
const mode = getIonMode(this);
const { el } = this;
const { el, mode } = this;
const animated = this.animated && config.getBoolean('animated', true);
const animationBuilder = this.animation || opts.animationBuilder || config.get('navAnimation');

View File

@ -2,6 +2,7 @@ import { Component, ComponentInterface, Listen, Prop, h } from '@stencil/core';
import { getIonMode } from '../../global/ionic-global';
import { SelectPopoverOption } from '../../interface';
import { safeCall } from '../../utils/overlays';
/**
* @internal
@ -28,8 +29,8 @@ export class SelectPopover implements ComponentInterface {
@Listen('ionSelect')
onSelect(ev: any) {
const option = this.options.find(o => o.value === ev.target.value);
if (option && option.handler) {
option.handler();
if (option) {
safeCall(option.handler);
}
}

View File

@ -34,9 +34,11 @@ export class TabBar implements ComponentInterface {
@Prop() selectedTab?: string;
@Watch('selectedTab')
selectedTabChanged() {
this.ionTabBarChanged.emit({
tab: this.selectedTab
});
if (this.selectedTab !== undefined) {
this.ionTabBarChanged.emit({
tab: this.selectedTab
});
}
}
/**

View File

@ -2,7 +2,7 @@ import { Component, ComponentInterface, Element, Event, EventEmitter, Method, Pr
import { getIonMode } from '../../global/ionic-global';
import { Animation, AnimationBuilder, Color, CssClassMap, OverlayEventDetail, OverlayInterface, ToastButton } from '../../interface';
import { dismiss, eventMethod, isCancel, present } from '../../utils/overlays';
import { dismiss, eventMethod, isCancel, present, safeCall } from '../../utils/overlays';
import { sanitizeDOMString } from '../../utils/sanitization';
import { createColorClasses, getClassMap } from '../../utils/theme';
@ -212,7 +212,7 @@ export class Toast implements ComponentInterface, OverlayInterface {
// a handler has been provided, execute it
// pass the handler the values from the inputs
try {
const rtn = await button.handler();
const rtn = await safeCall(button.handler);
if (rtn === false) {
// if the return value of the handler is false then do not dismiss
return false;

View File

@ -177,6 +177,7 @@ export interface IonicConfig {
persistConfig?: boolean;
_forceStatusbarPadding?: boolean;
_testing?: boolean;
_zoneGate?: (h: () => any) => any;
}
export function setupConfig(config: IonicConfig) {

View File

@ -203,16 +203,6 @@ const overlayAnimation = async (
return hasCompleted;
};
export const autoFocus = (containerEl: HTMLElement): HTMLElement | undefined => {
const focusableEls = containerEl.querySelectorAll('a[href], area[href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), button:not([disabled]), [tabindex="0"]');
if (focusableEls.length > 0) {
const el = focusableEls[0] as HTMLInputElement;
el.focus();
return el;
}
return undefined;
};
export const eventMethod = <T>(element: HTMLElement, eventName: string): Promise<T> => {
let resolve: (detail: T) => void;
const promise = new Promise<T>(r => resolve = r);
@ -244,4 +234,20 @@ const isDescendant = (parent: HTMLElement, child: HTMLElement | null) => {
return false;
};
const defaultGate = (h: any) => h();
export const safeCall = (handler: any, arg?: any) => {
if (typeof handler === 'function') {
const jmp = config.get('_zoneGate', defaultGate);
return jmp(() => {
try {
return handler(arg);
} catch (e) {
console.error(e);
}
});
}
return undefined;
};
export const BACKDROP = 'backdrop';