Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions core/api.txt
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@ ion-infinite-scroll-content,prop,theme,"ios" | "md" | "ionic",undefined,false,fa

ion-input,scoped
ion-input,prop,autocapitalize,string,'off',false,false
ion-input,prop,autocomplete,"name" | "email" | "tel" | "url" | "on" | "off" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "photo",'off',false,false
ion-input,prop,autocomplete,"name" | "url" | "off" | "on" | "additional-name" | "address-level1" | "address-level2" | "address-level3" | "address-level4" | "address-line1" | "address-line2" | "address-line3" | "bday-day" | "bday-month" | "bday-year" | "cc-csc" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-family-name" | "cc-given-name" | "cc-name" | "cc-number" | "cc-type" | "country" | "country-name" | "current-password" | "family-name" | "given-name" | "honorific-prefix" | "honorific-suffix" | "new-password" | "one-time-code" | "organization" | "postal-code" | "street-address" | "transaction-amount" | "transaction-currency" | "username" | "email" | "tel" | "tel-area-code" | "tel-country-code" | "tel-extension" | "tel-local" | "tel-national" | "nickname" | "organization-title" | "cc-additional-name" | "language" | "bday" | "sex" | "impp" | "photo",'off',false,false
ion-input,prop,autocorrect,"off" | "on",'off',false,false
ion-input,prop,autofocus,boolean,false,false,false
ion-input,prop,clearInput,boolean,false,false,false
Expand Down Expand Up @@ -1867,7 +1867,7 @@ ion-row,prop,theme,"ios" | "md" | "ionic",undefined,false,false
ion-searchbar,scoped
ion-searchbar,prop,animated,boolean,false,false,false
ion-searchbar,prop,autocapitalize,string,'off',false,false
ion-searchbar,prop,autocomplete,"name" | "email" | "tel" | "url" | "on" | "off" | "honorific-prefix" | "given-name" | "additional-name" | "family-name" | "honorific-suffix" | "nickname" | "username" | "new-password" | "current-password" | "one-time-code" | "organization-title" | "organization" | "street-address" | "address-line1" | "address-line2" | "address-line3" | "address-level4" | "address-level3" | "address-level2" | "address-level1" | "country" | "country-name" | "postal-code" | "cc-name" | "cc-given-name" | "cc-additional-name" | "cc-family-name" | "cc-number" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-csc" | "cc-type" | "transaction-currency" | "transaction-amount" | "language" | "bday" | "bday-day" | "bday-month" | "bday-year" | "sex" | "tel-country-code" | "tel-national" | "tel-area-code" | "tel-local" | "tel-extension" | "impp" | "photo",'off',false,false
ion-searchbar,prop,autocomplete,"name" | "url" | "off" | "on" | "additional-name" | "address-level1" | "address-level2" | "address-level3" | "address-level4" | "address-line1" | "address-line2" | "address-line3" | "bday-day" | "bday-month" | "bday-year" | "cc-csc" | "cc-exp" | "cc-exp-month" | "cc-exp-year" | "cc-family-name" | "cc-given-name" | "cc-name" | "cc-number" | "cc-type" | "country" | "country-name" | "current-password" | "family-name" | "given-name" | "honorific-prefix" | "honorific-suffix" | "new-password" | "one-time-code" | "organization" | "postal-code" | "street-address" | "transaction-amount" | "transaction-currency" | "username" | "email" | "tel" | "tel-area-code" | "tel-country-code" | "tel-extension" | "tel-local" | "tel-national" | "nickname" | "organization-title" | "cc-additional-name" | "language" | "bday" | "sex" | "impp" | "photo",'off',false,false
ion-searchbar,prop,autocorrect,"off" | "on",'off',false,false
ion-searchbar,prop,cancelButtonIcon,string | undefined,undefined,false,false
ion-searchbar,prop,cancelButtonText,string,'Cancel',false,false
Expand Down
87 changes: 4 additions & 83 deletions core/src/components/app/app.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import type { ComponentInterface } from '@stencil/core';
import { Build, Component, Element, Host, Method, h } from '@stencil/core';
import type { FocusVisibleUtility } from '@utils/focus-visible';
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
import { printIonWarning } from '@utils/logging';
import { isPlatform } from '@utils/platform';
import { Component, Element, Host, Method, h } from '@stencil/core';
import { getOrInitFocusVisibleUtility } from '@utils/focus-visible';

import { config } from '../../global/config';
import { getIonTheme } from '../../global/ionic-global';
Expand All @@ -17,53 +14,8 @@ import { getIonTheme } from '../../global/ionic-global';
styleUrl: 'app.scss',
})
export class App implements ComponentInterface {
private focusVisible?: FocusVisibleUtility;

@Element() el!: HTMLElement;

componentDidLoad() {
if (Build.isBrowser) {
rIC(async () => {
const isHybrid = isPlatform(window, 'hybrid');
if (!config.getBoolean('_testing')) {
import('../../utils/tap-click').then((module) => module.startTapClick(config));
}
if (config.getBoolean('statusTap', isHybrid)) {
import('../../utils/status-tap').then((module) => module.startStatusTap());
}
if (config.getBoolean('inputShims', needInputShims())) {
/**
* needInputShims() ensures that only iOS and Android
* platforms proceed into this block.
*/
const platform = isPlatform(window, 'ios') ? 'ios' : 'android';
import('../../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
}
const hardwareBackButtonModule = await import('../../utils/hardware-back-button');
const supportsHardwareBackButtonEvents = isHybrid || shouldUseCloseWatcher();
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
hardwareBackButtonModule.startHardwareBackButton();
} else {
/**
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
* then the close watcher will not be used.
*/
if (shouldUseCloseWatcher()) {
printIonWarning(
'experimentalCloseWatcher was set to `true`, but hardwareBackButton was set to `false`. Both config options must be `true` for the Close Watcher API to be used.'
);
}

hardwareBackButtonModule.blockHardwareBackButton();
}
if (typeof (window as any) !== 'undefined') {
import('../../utils/keyboard/keyboard').then((module) => module.startKeyboardAssist(window));
}
import('../../utils/focus-visible').then((module) => (this.focusVisible = module.startFocusVisible()));
});
}
}

/**
* @internal
* Used to set focus on an element that uses `ion-focusable`.
Expand All @@ -76,9 +28,8 @@ export class App implements ComponentInterface {
*/
@Method()
async setFocus(elements: HTMLElement[]) {
if (this.focusVisible) {
this.focusVisible.setFocus(elements);
}
const focusVisible = getOrInitFocusVisibleUtility();
focusVisible.setFocus(elements);
}

render() {
Expand All @@ -94,33 +45,3 @@ export class App implements ComponentInterface {
);
}
}

const needInputShims = () => {
/**
* iOS always needs input shims
*/
const needsShimsIOS = isPlatform(window, 'ios') && isPlatform(window, 'mobile');
if (needsShimsIOS) {
return true;
}

/**
* Android only needs input shims when running
* in the browser and only if the browser is using the
* new Chrome 108+ resize behavior: https://developer.chrome.com/blog/viewport-resize-behavior/
*/
const isAndroidMobileWeb = isPlatform(window, 'android') && isPlatform(window, 'mobileweb');
if (isAndroidMobileWeb) {
return true;
}

return false;
};

const rIC = (callback: () => void) => {
if ('requestIdleCallback' in window) {
(window as any).requestIdleCallback(callback);
} else {
setTimeout(callback, 32);
}
};
4 changes: 3 additions & 1 deletion core/src/components/content/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isPlatform } from '@utils/platform';
import { isRTL } from '@utils/rtl';
import { createColorClasses, hostContext } from '@utils/theme';

import { config } from '../../global/config';
import { getIonMode, getIonTheme } from '../../global/ionic-global';
import type { Color, Mode } from '../../interface';

Expand Down Expand Up @@ -518,7 +519,8 @@ const getPageElement = (el: HTMLElement) => {
* between the popover and the edges of the screen. But if the popover contains
* its own page element, we should use that instead.
*/
const page = el.closest('ion-app, ion-page, .ion-page, page-inner, .popover-content');
const appRootSelector = config.get('appRootSelector', 'ion-app');
const page = el.closest(`${appRootSelector}, ion-page, .ion-page, page-inner, .popover-content`);
if (page) {
return page;
}
Expand Down
4 changes: 3 additions & 1 deletion core/src/components/footer/footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { findIonContent, getScrollElement, printIonContentErrorMsg } from '@util
import type { KeyboardController } from '@utils/keyboard/keyboard-controller';
import { createKeyboardController } from '@utils/keyboard/keyboard-controller';

import { config } from '../../global/config';
import { getIonTheme } from '../../global/ionic-global';

import { handleFooterFade } from './footer.utils';
Expand Down Expand Up @@ -86,7 +87,8 @@ export class Footer implements ComponentInterface {
this.destroyCollapsibleFooter();

if (hasFade) {
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const appRootSelector = config.get('appRootSelector', 'ion-app');
const pageEl = this.el.closest(`${appRootSelector},ion-page,.ion-page,page-inner`);
const contentEl = pageEl ? findIonContent(pageEl) : null;

if (!contentEl) {
Expand Down
7 changes: 5 additions & 2 deletions core/src/components/header/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import type { Attributes } from '@utils/helpers';
import { inheritAriaAttributes } from '@utils/helpers';
import { hostContext } from '@utils/theme';

import { config } from '../../global/config';
import { getIonTheme } from '../../global/ionic-global';

import {
Expand Down Expand Up @@ -91,8 +92,10 @@ export class Header implements ComponentInterface {

this.destroyCollapsibleHeader();

const appRootSelector = config.get('appRootSelector', 'ion-app');

if (hasCondense) {
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const pageEl = this.el.closest(`${appRootSelector},ion-page,.ion-page,page-inner`);
const contentEl = pageEl ? findIonContent(pageEl) : null;

// Cloned elements are always needed in iOS transition
Expand All @@ -104,7 +107,7 @@ export class Header implements ComponentInterface {

await this.setupCondenseHeader(contentEl, pageEl);
} else if (hasFade) {
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
const pageEl = this.el.closest(`${appRootSelector},ion-page,.ion-page,page-inner`);
const contentEl = pageEl ? findIonContent(pageEl) : null;

if (!contentEl) {
Expand Down
77 changes: 76 additions & 1 deletion core/src/global/ionic-global.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { getMode, setMode, setPlatformHelpers, getElement } from '@stencil/core';
import { Build, getMode, setMode, setPlatformHelpers, getElement } from '@stencil/core';
import { printIonWarning } from '@utils/logging';

import type { IonicConfig, Mode, Theme } from '../interface';
import { shouldUseCloseWatcher } from '../utils/hardware-back-button';
import { isPlatform, setupPlatforms } from '../utils/platform';

import { config, configFromSession, configFromURL, saveConfig } from './config';
Expand Down Expand Up @@ -131,6 +132,36 @@ export const getIonTheme = (ref?: any): Theme => {
return defaultTheme;
};

export const rIC = (callback: () => void) => {
if ('requestIdleCallback' in window) {
(window as any).requestIdleCallback(callback);
} else {
setTimeout(callback, 32);
}
};

export const needInputShims = () => {
/**
* iOS always needs input shims
*/
const needsShimsIOS = isPlatform(window, 'ios') && isPlatform(window, 'mobile');
if (needsShimsIOS) {
return true;
}

/**
* Android only needs input shims when running
* in the browser and only if the browser is using the
* new Chrome 108+ resize behavior: https://developer.chrome.com/blog/viewport-resize-behavior/
*/
const isAndroidMobileWeb = isPlatform(window, 'android') && isPlatform(window, 'mobileweb');
if (isAndroidMobileWeb) {
return true;
}

return false;
};

export const initialize = (userConfig: IonicConfig = {}) => {
if (typeof (window as any) === 'undefined') {
return;
Expand Down Expand Up @@ -255,6 +286,50 @@ export const initialize = (userConfig: IonicConfig = {}) => {
}
return defaultTheme;
});

// `IonApp` code
// ----------------------------------------------

if (Build.isBrowser) {
rIC(async () => {
const isHybrid = isPlatform(window, 'hybrid');
if (!config.getBoolean('_testing')) {
import('../utils/tap-click').then((module) => module.startTapClick(config));
}
if (config.getBoolean('statusTap', isHybrid)) {
import('../utils/status-tap').then((module) => module.startStatusTap());
}
if (config.getBoolean('inputShims', needInputShims())) {
/**
* needInputShims() ensures that only iOS and Android
* platforms proceed into this block.
*/
const platform = isPlatform(window, 'ios') ? 'ios' : 'android';
import('../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
}
const hardwareBackButtonModule = await import('../utils/hardware-back-button');
const supportsHardwareBackButtonEvents = isHybrid || shouldUseCloseWatcher();
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
hardwareBackButtonModule.startHardwareBackButton();
} else {
/**
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
* then the close watcher will not be used.
*/
if (shouldUseCloseWatcher()) {
printIonWarning(
'experimentalCloseWatcher was set to `true`, but hardwareBackButton was set to `false`. Both config options must be `true` for the Close Watcher API to be used.'
);
}

hardwareBackButtonModule.blockHardwareBackButton();
}
if (typeof (window as any) !== 'undefined') {
import('../utils/keyboard/keyboard').then((module) => module.startKeyboardAssist(window));
}
import('../utils/focus-visible').then((module) => module.getOrInitFocusVisibleUtility());
});
}
};

export default initialize;
8 changes: 8 additions & 0 deletions core/src/utils/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ export interface IonicConfig {
*/
toastDuration?: number;

/**
* The selector that will be used to query the root of the Ionic application.
* This element is used for things like injecting overlay elements into the DOM and managing focus.
*
* @default 'ion-app'
*/
appRootSelector?: string;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For context, we needed to add this option because without ion-app, our overlay utils would default to injecting overlay element into the body of the DOM. However, this could cause problems in Frameworks like React where the actual React application was initialized inside a container in the DOM like div id="root", so if the overlay were outside that container, things like event listeners would not work.

With this option, devs can specify where the overlay should get injected using any valid CSS selector


/**
* Overrides the toggle icon for all `ion-accordion` components.
*/
Expand Down
10 changes: 10 additions & 0 deletions core/src/utils/focus-visible.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ export interface FocusVisibleUtility {
setFocus: (elements: Element[]) => void;
}

let focusVisibleUtility: FocusVisibleUtility | null = null;

export const getOrInitFocusVisibleUtility = () => {
if (!focusVisibleUtility) {
focusVisibleUtility = startFocusVisible();
}

return focusVisibleUtility;
};

export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => {
let currentFocus: Element[] = [];
let keyboardMode = true;
Expand Down
4 changes: 3 additions & 1 deletion core/src/utils/framework-delegate.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { config } from '../global/config';
import type { ComponentRef, FrameworkDelegate } from '../interface';

import { componentOnReady } from './helpers';
Expand Down Expand Up @@ -128,7 +129,8 @@ export const CoreDelegate = () => {
* Get the root of the app and
* add the overlay there.
*/
const app = document.querySelector('ion-app') || document.body;
const appRootSelector = config.get('appRootSelector', 'ion-app');
const app = document.querySelector(appRootSelector) || document.body;

/**
* Create a placeholder comment so that
Expand Down
4 changes: 3 additions & 1 deletion core/src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { EventEmitter } from '@stencil/core';

import type { Side } from '../components/menu/menu-interface';
import { config } from '../global/config';

// TODO(FW-2832): types

Expand Down Expand Up @@ -266,7 +267,8 @@ export const focusVisibleElement = (el: HTMLElement) => {
* which will let us explicitly set the elements to focus.
*/
if (el.classList.contains('ion-focusable')) {
const app = el.closest('ion-app');
const appRootSelector = config.get('appRootSelector', 'ion-app');
const app = el.closest(appRootSelector) as HTMLIonAppElement | null;
if (app) {
app.setFocus([el]);
}
Expand Down
4 changes: 3 additions & 1 deletion core/src/utils/keyboard/keyboard-controller.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { doc, win } from '@utils/browser';

import { config } from '../../global/config';
import { Keyboard, KeyboardResize } from '../native/keyboard';

/**
Expand All @@ -25,7 +26,8 @@ const getResizeContainer = (resizeMode?: KeyboardResize): HTMLElement | null =>
* on that. In the event `ion-app` is not available then
* we can fall back to `body`.
*/
const ionApp = doc.querySelector('ion-app');
const appRootSelector = config.get('appRootSelector', 'ion-app');
const ionApp = doc.querySelector(appRootSelector) as HTMLIonAppElement | null;

return ionApp ?? doc.body;
};
Expand Down
3 changes: 2 additions & 1 deletion core/src/utils/overlays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,8 @@ export const dismiss = async <OverlayDismissOptions>(
};

const getAppRoot = (doc: Document) => {
return doc.querySelector('ion-app') || doc.body;
const appRootSelector = config.get('appRootSelector', 'ion-app');
return doc.querySelector(appRootSelector) || doc.body;
};

const overlayAnimation = async (
Expand Down
Loading