Skip to content

Commit 15d6104

Browse files
feat(app): move ion-app init logic to initialize function (#29930)
Issue number: resolves internal --------- <!-- Please do not submit updates to dependencies unless it fixes an issue. --> <!-- Please try to limit your pull request to one type (bugfix, feature, etc). Submit multiple pull requests if needed. --> ## What is the current behavior? <!-- Please describe the current behavior that you are modifying. --> Some app functionality (like focus management, keyboard utils, and shimming) are tied to the `ion-app` which requires all Ionic applications to have a root `ion-app` element. ## What is the new behavior? <!-- Please describe the behavior or changes that are being added by this PR. --> `ion-app` specific init functionality is moved to the global `initialize` function ## Does this introduce a breaking change? - [ ] Yes - [x] No <!-- If this introduces a breaking change: 1. Describe the impact and migration path for existing applications below. 2. Update the BREAKING.md file with the breaking change. 3. Add "BREAKING CHANGE: [...]" to the commit description when merging. See https://github.com/ionic-team/ionic-framework/blob/main/docs/CONTRIBUTING.md#footer for more information. --> Although it is not expected that this introduces a breaking change, these changes were introduced on the `next` branch as a precaution. ## Other information <!-- Any other information that is important to this PR such as screenshots of how the component looks before and after the change. --> NOTE: This is **NOT** a recommended pattern for Ionic applications and was created for a specific internal use case --------- Co-authored-by: Brandy Carney <[email protected]>
1 parent 5d4a989 commit 15d6104

File tree

12 files changed

+122
-94
lines changed

12 files changed

+122
-94
lines changed

core/api.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ ion-infinite-scroll-content,prop,theme,"ios" | "md" | "ionic",undefined,false,fa
884884

885885
ion-input,scoped
886886
ion-input,prop,autocapitalize,string,'off',false,false
887-
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
887+
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
888888
ion-input,prop,autocorrect,"off" | "on",'off',false,false
889889
ion-input,prop,autofocus,boolean,false,false,false
890890
ion-input,prop,clearInput,boolean,false,false,false
@@ -1867,7 +1867,7 @@ ion-row,prop,theme,"ios" | "md" | "ionic",undefined,false,false
18671867
ion-searchbar,scoped
18681868
ion-searchbar,prop,animated,boolean,false,false,false
18691869
ion-searchbar,prop,autocapitalize,string,'off',false,false
1870-
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
1870+
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
18711871
ion-searchbar,prop,autocorrect,"off" | "on",'off',false,false
18721872
ion-searchbar,prop,cancelButtonIcon,string | undefined,undefined,false,false
18731873
ion-searchbar,prop,cancelButtonText,string,'Cancel',false,false

core/src/components/app/app.tsx

Lines changed: 4 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
import type { ComponentInterface } from '@stencil/core';
2-
import { Build, Component, Element, Host, Method, h } from '@stencil/core';
3-
import type { FocusVisibleUtility } from '@utils/focus-visible';
4-
import { shouldUseCloseWatcher } from '@utils/hardware-back-button';
5-
import { printIonWarning } from '@utils/logging';
6-
import { isPlatform } from '@utils/platform';
2+
import { Component, Element, Host, Method, h } from '@stencil/core';
3+
import { getOrInitFocusVisibleUtility } from '@utils/focus-visible';
74

85
import { config } from '../../global/config';
96
import { getIonTheme } from '../../global/ionic-global';
@@ -17,53 +14,8 @@ import { getIonTheme } from '../../global/ionic-global';
1714
styleUrl: 'app.scss',
1815
})
1916
export class App implements ComponentInterface {
20-
private focusVisible?: FocusVisibleUtility;
21-
2217
@Element() el!: HTMLElement;
2318

24-
componentDidLoad() {
25-
if (Build.isBrowser) {
26-
rIC(async () => {
27-
const isHybrid = isPlatform(window, 'hybrid');
28-
if (!config.getBoolean('_testing')) {
29-
import('../../utils/tap-click').then((module) => module.startTapClick(config));
30-
}
31-
if (config.getBoolean('statusTap', isHybrid)) {
32-
import('../../utils/status-tap').then((module) => module.startStatusTap());
33-
}
34-
if (config.getBoolean('inputShims', needInputShims())) {
35-
/**
36-
* needInputShims() ensures that only iOS and Android
37-
* platforms proceed into this block.
38-
*/
39-
const platform = isPlatform(window, 'ios') ? 'ios' : 'android';
40-
import('../../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
41-
}
42-
const hardwareBackButtonModule = await import('../../utils/hardware-back-button');
43-
const supportsHardwareBackButtonEvents = isHybrid || shouldUseCloseWatcher();
44-
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
45-
hardwareBackButtonModule.startHardwareBackButton();
46-
} else {
47-
/**
48-
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
49-
* then the close watcher will not be used.
50-
*/
51-
if (shouldUseCloseWatcher()) {
52-
printIonWarning(
53-
'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.'
54-
);
55-
}
56-
57-
hardwareBackButtonModule.blockHardwareBackButton();
58-
}
59-
if (typeof (window as any) !== 'undefined') {
60-
import('../../utils/keyboard/keyboard').then((module) => module.startKeyboardAssist(window));
61-
}
62-
import('../../utils/focus-visible').then((module) => (this.focusVisible = module.startFocusVisible()));
63-
});
64-
}
65-
}
66-
6719
/**
6820
* @internal
6921
* Used to set focus on an element that uses `ion-focusable`.
@@ -76,9 +28,8 @@ export class App implements ComponentInterface {
7628
*/
7729
@Method()
7830
async setFocus(elements: HTMLElement[]) {
79-
if (this.focusVisible) {
80-
this.focusVisible.setFocus(elements);
81-
}
31+
const focusVisible = getOrInitFocusVisibleUtility();
32+
focusVisible.setFocus(elements);
8233
}
8334

8435
render() {
@@ -94,33 +45,3 @@ export class App implements ComponentInterface {
9445
);
9546
}
9647
}
97-
98-
const needInputShims = () => {
99-
/**
100-
* iOS always needs input shims
101-
*/
102-
const needsShimsIOS = isPlatform(window, 'ios') && isPlatform(window, 'mobile');
103-
if (needsShimsIOS) {
104-
return true;
105-
}
106-
107-
/**
108-
* Android only needs input shims when running
109-
* in the browser and only if the browser is using the
110-
* new Chrome 108+ resize behavior: https://developer.chrome.com/blog/viewport-resize-behavior/
111-
*/
112-
const isAndroidMobileWeb = isPlatform(window, 'android') && isPlatform(window, 'mobileweb');
113-
if (isAndroidMobileWeb) {
114-
return true;
115-
}
116-
117-
return false;
118-
};
119-
120-
const rIC = (callback: () => void) => {
121-
if ('requestIdleCallback' in window) {
122-
(window as any).requestIdleCallback(callback);
123-
} else {
124-
setTimeout(callback, 32);
125-
}
126-
};

core/src/components/content/content.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { isPlatform } from '@utils/platform';
66
import { isRTL } from '@utils/rtl';
77
import { createColorClasses, hostContext } from '@utils/theme';
88

9+
import { config } from '../../global/config';
910
import { getIonMode, getIonTheme } from '../../global/ionic-global';
1011
import type { Color, Mode } from '../../interface';
1112

@@ -518,7 +519,8 @@ const getPageElement = (el: HTMLElement) => {
518519
* between the popover and the edges of the screen. But if the popover contains
519520
* its own page element, we should use that instead.
520521
*/
521-
const page = el.closest('ion-app, ion-page, .ion-page, page-inner, .popover-content');
522+
const appRootSelector = config.get('appRootSelector', 'ion-app');
523+
const page = el.closest(`${appRootSelector}, ion-page, .ion-page, page-inner, .popover-content`);
522524
if (page) {
523525
return page;
524526
}

core/src/components/footer/footer.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { findIonContent, getScrollElement, printIonContentErrorMsg } from '@util
44
import type { KeyboardController } from '@utils/keyboard/keyboard-controller';
55
import { createKeyboardController } from '@utils/keyboard/keyboard-controller';
66

7+
import { config } from '../../global/config';
78
import { getIonTheme } from '../../global/ionic-global';
89

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

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

9294
if (!contentEl) {

core/src/components/header/header.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { Attributes } from '@utils/helpers';
55
import { inheritAriaAttributes } from '@utils/helpers';
66
import { hostContext } from '@utils/theme';
77

8+
import { config } from '../../global/config';
89
import { getIonTheme } from '../../global/ionic-global';
910

1011
import {
@@ -91,8 +92,10 @@ export class Header implements ComponentInterface {
9192

9293
this.destroyCollapsibleHeader();
9394

95+
const appRootSelector = config.get('appRootSelector', 'ion-app');
96+
9497
if (hasCondense) {
95-
const pageEl = this.el.closest('ion-app,ion-page,.ion-page,page-inner');
98+
const pageEl = this.el.closest(`${appRootSelector},ion-page,.ion-page,page-inner`);
9699
const contentEl = pageEl ? findIonContent(pageEl) : null;
97100

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

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

110113
if (!contentEl) {

core/src/global/ionic-global.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { getMode, setMode, setPlatformHelpers, getElement } from '@stencil/core';
1+
import { Build, getMode, setMode, setPlatformHelpers, getElement } from '@stencil/core';
22
import { printIonWarning } from '@utils/logging';
33

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

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

135+
export const rIC = (callback: () => void) => {
136+
if ('requestIdleCallback' in window) {
137+
(window as any).requestIdleCallback(callback);
138+
} else {
139+
setTimeout(callback, 32);
140+
}
141+
};
142+
143+
export const needInputShims = () => {
144+
/**
145+
* iOS always needs input shims
146+
*/
147+
const needsShimsIOS = isPlatform(window, 'ios') && isPlatform(window, 'mobile');
148+
if (needsShimsIOS) {
149+
return true;
150+
}
151+
152+
/**
153+
* Android only needs input shims when running
154+
* in the browser and only if the browser is using the
155+
* new Chrome 108+ resize behavior: https://developer.chrome.com/blog/viewport-resize-behavior/
156+
*/
157+
const isAndroidMobileWeb = isPlatform(window, 'android') && isPlatform(window, 'mobileweb');
158+
if (isAndroidMobileWeb) {
159+
return true;
160+
}
161+
162+
return false;
163+
};
164+
134165
export const initialize = (userConfig: IonicConfig = {}) => {
135166
if (typeof (window as any) === 'undefined') {
136167
return;
@@ -255,6 +286,50 @@ export const initialize = (userConfig: IonicConfig = {}) => {
255286
}
256287
return defaultTheme;
257288
});
289+
290+
// `IonApp` code
291+
// ----------------------------------------------
292+
293+
if (Build.isBrowser) {
294+
rIC(async () => {
295+
const isHybrid = isPlatform(window, 'hybrid');
296+
if (!config.getBoolean('_testing')) {
297+
import('../utils/tap-click').then((module) => module.startTapClick(config));
298+
}
299+
if (config.getBoolean('statusTap', isHybrid)) {
300+
import('../utils/status-tap').then((module) => module.startStatusTap());
301+
}
302+
if (config.getBoolean('inputShims', needInputShims())) {
303+
/**
304+
* needInputShims() ensures that only iOS and Android
305+
* platforms proceed into this block.
306+
*/
307+
const platform = isPlatform(window, 'ios') ? 'ios' : 'android';
308+
import('../utils/input-shims/input-shims').then((module) => module.startInputShims(config, platform));
309+
}
310+
const hardwareBackButtonModule = await import('../utils/hardware-back-button');
311+
const supportsHardwareBackButtonEvents = isHybrid || shouldUseCloseWatcher();
312+
if (config.getBoolean('hardwareBackButton', supportsHardwareBackButtonEvents)) {
313+
hardwareBackButtonModule.startHardwareBackButton();
314+
} else {
315+
/**
316+
* If an app sets hardwareBackButton: false and experimentalCloseWatcher: true
317+
* then the close watcher will not be used.
318+
*/
319+
if (shouldUseCloseWatcher()) {
320+
printIonWarning(
321+
'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.'
322+
);
323+
}
324+
325+
hardwareBackButtonModule.blockHardwareBackButton();
326+
}
327+
if (typeof (window as any) !== 'undefined') {
328+
import('../utils/keyboard/keyboard').then((module) => module.startKeyboardAssist(window));
329+
}
330+
import('../utils/focus-visible').then((module) => module.getOrInitFocusVisibleUtility());
331+
});
332+
}
258333
};
259334

260335
export default initialize;

core/src/utils/config.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ export interface IonicConfig {
124124
*/
125125
toastDuration?: number;
126126

127+
/**
128+
* The selector that will be used to query the root of the Ionic application.
129+
* This element is used for things like injecting overlay elements into the DOM and managing focus.
130+
*
131+
* @default 'ion-app'
132+
*/
133+
appRootSelector?: string;
134+
127135
/**
128136
* Overrides the toggle icon for all `ion-accordion` components.
129137
*/

core/src/utils/focus-visible.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ export interface FocusVisibleUtility {
2020
setFocus: (elements: Element[]) => void;
2121
}
2222

23+
let focusVisibleUtility: FocusVisibleUtility | null = null;
24+
25+
export const getOrInitFocusVisibleUtility = () => {
26+
if (!focusVisibleUtility) {
27+
focusVisibleUtility = startFocusVisible();
28+
}
29+
30+
return focusVisibleUtility;
31+
};
32+
2333
export const startFocusVisible = (rootEl?: HTMLElement): FocusVisibleUtility => {
2434
let currentFocus: Element[] = [];
2535
let keyboardMode = true;

core/src/utils/framework-delegate.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { config } from '../global/config';
12
import type { ComponentRef, FrameworkDelegate } from '../interface';
23

34
import { componentOnReady } from './helpers';
@@ -128,7 +129,8 @@ export const CoreDelegate = () => {
128129
* Get the root of the app and
129130
* add the overlay there.
130131
*/
131-
const app = document.querySelector('ion-app') || document.body;
132+
const appRootSelector = config.get('appRootSelector', 'ion-app');
133+
const app = document.querySelector(appRootSelector) || document.body;
132134

133135
/**
134136
* Create a placeholder comment so that

core/src/utils/helpers.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { EventEmitter } from '@stencil/core';
22

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

56
// TODO(FW-2832): types
67

@@ -266,7 +267,8 @@ export const focusVisibleElement = (el: HTMLElement) => {
266267
* which will let us explicitly set the elements to focus.
267268
*/
268269
if (el.classList.contains('ion-focusable')) {
269-
const app = el.closest('ion-app');
270+
const appRootSelector = config.get('appRootSelector', 'ion-app');
271+
const app = el.closest(appRootSelector) as HTMLIonAppElement | null;
270272
if (app) {
271273
app.setFocus([el]);
272274
}

0 commit comments

Comments
 (0)