@rdlabo/ionic-theme-ios27 1.2.0 → 1.2.1-beta.pr273.shae866fb1dca9f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/css/components/ion-button.css +1 -1
- package/dist/css/components/ion-toolbar.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/vertical-bars.css +1 -1
- package/dist/native/components/index.js +2 -2
- package/dist/native/components/index.js.map +1 -1
- package/dist/native/definitions.d.ts +32 -0
- package/dist/native/definitions.d.ts.map +1 -1
- package/dist/native/index.d.ts.map +1 -1
- package/dist/native/index.js +23 -6
- package/dist/native/index.js.map +1 -1
- package/dist/native/overlays/content.d.ts +4 -0
- package/dist/native/overlays/content.d.ts.map +1 -0
- package/dist/native/overlays/content.js +51 -0
- package/dist/native/overlays/content.js.map +1 -0
- package/dist/native/overlays/controller.d.ts +8 -0
- package/dist/native/overlays/controller.d.ts.map +1 -0
- package/dist/native/overlays/controller.js +266 -0
- package/dist/native/overlays/controller.js.map +1 -0
- package/dist/native/overlays/dialog-animation.d.ts +8 -0
- package/dist/native/overlays/dialog-animation.d.ts.map +1 -0
- package/dist/native/overlays/dialog-animation.js +31 -0
- package/dist/native/overlays/dialog-animation.js.map +1 -0
- package/dist/native/overlays/dismiss-animation.d.ts +4 -0
- package/dist/native/overlays/dismiss-animation.d.ts.map +1 -0
- package/dist/native/overlays/dismiss-animation.js +18 -0
- package/dist/native/overlays/dismiss-animation.js.map +1 -0
- package/dist/native/overlays/focus.d.ts +5 -0
- package/dist/native/overlays/focus.d.ts.map +1 -0
- package/dist/native/overlays/focus.js +59 -0
- package/dist/native/overlays/focus.js.map +1 -0
- package/dist/native/overlays/popover.d.ts +11 -0
- package/dist/native/overlays/popover.d.ts.map +1 -0
- package/dist/native/overlays/popover.js +78 -0
- package/dist/native/overlays/popover.js.map +1 -0
- package/dist/native/overlays/styles.d.ts +9 -0
- package/dist/native/overlays/styles.d.ts.map +1 -0
- package/dist/native/overlays/styles.js +82 -0
- package/dist/native/overlays/styles.js.map +1 -0
- package/dist/native/prehide.d.ts.map +1 -1
- package/dist/native/prehide.js +6 -2
- package/dist/native/prehide.js.map +1 -1
- package/dist/native/runtime.d.ts +4 -1
- package/dist/native/runtime.d.ts.map +1 -1
- package/dist/native/runtime.js +29 -8
- package/dist/native/runtime.js.map +1 -1
- package/dist/native/shared/dom.d.ts +2 -0
- package/dist/native/shared/dom.d.ts.map +1 -1
- package/dist/native/shared/dom.js +5 -1
- package/dist/native/shared/dom.js.map +1 -1
- package/dist/native/shared/projection-id.d.ts +4 -0
- package/dist/native/shared/projection-id.d.ts.map +1 -0
- package/dist/native/shared/projection-id.js +7 -0
- package/dist/native/shared/projection-id.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -1
- package/dist/native/vertical-bars-web.js +4 -2
- package/dist/native/vertical-bars-web.js.map +1 -1
- package/dist/native-integration/index.d.ts +1 -1
- package/dist/native-integration/index.js.map +1 -1
- package/dist/native-integration/transition.d.ts +1 -1
- package/dist/native-integration/transition.js.map +1 -1
- package/dist/sheets-of-glass/animations.d.ts +1 -1
- package/dist/sheets-of-glass/animations.js +1 -1
- package/dist/sheets-of-glass/animations.js.map +1 -1
- package/dist/sheets-of-glass/index.js +1 -1
- package/dist/sheets-of-glass/index.js.map +1 -1
- package/dist/tab-bar-searchable/animations/enter.d.ts +1 -1
- package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -1
- package/dist/tab-bar-searchable/animations/enter.js +1 -1
- package/dist/tab-bar-searchable/animations/enter.js.map +1 -1
- package/dist/tab-bar-searchable/animations/leave.d.ts +1 -1
- package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -1
- package/dist/tab-bar-searchable/animations/leave.js +1 -1
- package/dist/tab-bar-searchable/animations/leave.js.map +1 -1
- package/dist/tab-bar-searchable/index.js +1 -1
- package/dist/tab-bar-searchable/index.js.map +1 -1
- package/dist/transition/index.d.ts +1 -1
- package/dist/transition/index.js +1 -1
- package/dist/transition/index.js.map +1 -1
- package/dist/transition/ios.transition.d.ts +1 -1
- package/dist/transition/ios.transition.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/utils.js.map +1 -1
- package/docs/native-ui-shell.md +20 -4
- package/docs/vertical-bars.md +3 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +65 -91
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +9 -3
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +79 -3
- package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +57 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayController.swift +116 -0
- package/ios/Sources/IonicNativeUIShellPlugin/Overlays/ShellOverlayHost.swift +36 -0
- package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +27 -0
- package/package.json +2 -2
- package/src/native/components/index.ts +3 -2
- package/src/native/definitions.ts +27 -1
- package/src/native/index.ts +23 -6
- package/src/native/overlays/content.ts +51 -0
- package/src/native/overlays/controller.ts +322 -0
- package/src/native/overlays/dialog-animation.ts +30 -0
- package/src/native/overlays/dismiss-animation.ts +15 -0
- package/src/native/overlays/focus.ts +64 -0
- package/src/native/overlays/popover.ts +80 -0
- package/src/native/overlays/styles.ts +80 -0
- package/src/native/prehide.ts +6 -1
- package/src/native/runtime.ts +39 -15
- package/src/native/shared/dom.ts +14 -0
- package/src/native/shared/projection-id.ts +2 -0
- package/src/native/vertical-bars-web.ts +11 -1
- package/src/native-integration/index.ts +1 -1
- package/src/native-integration/transition.ts +1 -1
- package/src/sheets-of-glass/animations.ts +2 -2
- package/src/sheets-of-glass/index.ts +2 -2
- package/src/styles/components/ion-button.scss +47 -0
- package/src/styles/components/ion-toolbar.scss +62 -7
- package/src/styles/vertical-bars.scss +23 -0
- package/src/tab-bar-searchable/animations/enter.ts +2 -1
- package/src/tab-bar-searchable/animations/leave.ts +2 -1
- package/src/tab-bar-searchable/index.ts +1 -1
- package/src/transition/index.ts +3 -3
- package/src/transition/ios.transition.ts +1 -1
- package/src/utils.ts +1 -1
|
@@ -2,7 +2,7 @@ import { isNativeUIShell, registerNativeSearch, requestNativeSearch, suspendNati
|
|
|
2
2
|
import { addSearchDismissButton } from "./dismiss-button.js";
|
|
3
3
|
import { createCloseButtonsAnimation, createEffectAnimation, createFabButtonAnimation, createSearchContainerAnimation, createTabBarAnimation } from "./animations/enter.js";
|
|
4
4
|
import { createReverseCloseButtonsAnimation, createReverseEffectAnimation, createReverseFabButtonAnimation, createReverseSearchContainerAnimation, createReverseTabBarAnimation } from "./animations/leave.js";
|
|
5
|
-
import { createAnimation } from "@ionic/core";
|
|
5
|
+
import { createAnimation } from "@ionic/core/components/index.js";
|
|
6
6
|
import { ANIMATION_DELAY_BASE, ANIMATION_DURATION, ANIMATION_EASING, TabBarSearchableType, TabBarSearchableType as TabBarSearchableType$1, getElementReferences, getElementSizes, throwErrorByFailedClickElement } from "@rdlabo/ionic-theme-utils";
|
|
7
7
|
//#region src/tab-bar-searchable/index.ts
|
|
8
8
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":["TabBarSearchableType"],"sources":["../../src/tab-bar-searchable/index.ts"],"sourcesContent":["import { createAnimation } from '@ionic/core';\nimport { addSearchDismissButton } from './dismiss-button';\nimport {\n ANIMATION_DELAY_BASE,\n ANIMATION_DURATION,\n ANIMATION_EASING,\n getElementReferences,\n getElementSizes,\n throwErrorByFailedClickElement,\n type SearchableEventCache,\n type TabBarSearchableFunction,\n TabBarSearchableType,\n} from '@rdlabo/ionic-theme-utils';\nimport { isNativeUIShell, registerNativeSearch, requestNativeSearch, suspendNativeUIShell } from '../native-integration';\nimport {\n createCloseButtonsAnimation,\n createEffectAnimation,\n createFabButtonAnimation,\n createSearchContainerAnimation,\n createTabBarAnimation,\n} from './animations/enter';\nimport {\n createReverseCloseButtonsAnimation,\n createReverseEffectAnimation,\n createReverseFabButtonAnimation,\n createReverseSearchContainerAnimation,\n createReverseTabBarAnimation,\n} from './animations/leave';\n\nexport {\n TabBarSearchableType,\n type TabBarSearchableFunction,\n type SearchableEventCache,\n type ElementSizes,\n type ElementReferences,\n} from '@rdlabo/ionic-theme-utils';\n\n/**\n * <ion-fab vertical=\"bottom\" horizontal=\"end\" slot=\"fixed\">\n * <ion-fab-button (click)=\"present($event)\">\n * <ion-icon name=\"search\"></ion-icon>\n * </ion-fab-button>\n * </ion-fab>\n * <ion-footer [translucent]=\"true\">\n * <ion-toolbar>\n * <ion-buttons slot=\"start\">\n * <!-- ion-icon does not need `name` attribute. -->\n * <ion-button fill=\"default\"><ion-icon slot=\"icon-only\"></ion-icon>\n * </ion-button>\n * </ion-buttons>\n * <!-- User set `ionChange` or other events. -->\n * <ion-searchbar aria-label=\"Search\" (ionChange)=\"example($event)\"></ion-searchbar>\n * </ion-toolbar>\n * </ion-footer>\n **/\n\nexport const attachTabBarSearchable = (\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n): TabBarSearchableFunction => {\n if (!ionTabBar || !ionFabButton || !ionFooter) {\n throw new Error('TabBarSearchable should be defined with props');\n }\n // Initialize\n ionFooter.classList.add('ios-theme-searchable');\n ionFabButton.closest('ion-fab')?.classList.add('ios-theme-searchable-trigger');\n addSearchDismissButton(ionFooter);\n ionFooter.style.pointerEvents = 'none';\n ionFooter.style.opacity = '0';\n // Search dismissal belongs beside the Web search field, never in the vertical rail.\n ionFooter.querySelector('ion-buttons[slot=start]')?.classList.add('ios-theme-horizontal-only');\n const nativeSearch = registerNativeSearch(ionTabBar, ionFabButton, ionFooter);\n\n // Saved Params\n let searchableEventCache: SearchableEventCache | undefined;\n let resumeNative: (() => void) | undefined;\n // Leave is valid both while native Enter awaits its ack and after it completes.\n let nativeEntry = false;\n let nativeRequest = 0;\n let verticalSearch = false;\n let tabVisibilityObserver: MutationObserver | undefined;\n\n return async (event: Event, type: TabBarSearchableType) => {\n const selector = type === TabBarSearchableType.Enter ? 'ion-fab-button' : 'ion-buttons[slot=start] ion-button';\n if (!(event.target as HTMLElement)?.closest(selector)) throw throwErrorByFailedClickElement(selector);\n if (type === TabBarSearchableType.Leave && !nativeEntry && !searchableEventCache)\n throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');\n const hadNativeEntry = nativeEntry;\n const request = ++nativeRequest;\n if (type === TabBarSearchableType.Enter) nativeEntry ||= isNativeUIShell(ionFooter);\n const accepted = await requestNativeSearch(nativeSearch, type === TabBarSearchableType.Enter);\n if (request !== nativeRequest) return;\n if (accepted) {\n nativeEntry = type === TabBarSearchableType.Enter;\n return;\n }\n nativeEntry = false;\n if (type === TabBarSearchableType.Leave && hadNativeEntry) {\n // Forced native retirement already restored the closed Web presentation.\n return;\n }\n if (type === TabBarSearchableType.Enter) {\n verticalSearch = !!ionTabBar.closest('ion-app.ios-theme-vertical-bars');\n if (verticalSearch) {\n // Ionic hides horizontal tabs when the keyboard opens; keep this rail available.\n const keepTabsVisible = () => {\n if (ionTabBar.classList.contains('tab-bar-hidden') && ionTabBar.closest('ion-app.ios-theme-vertical-bars')) {\n ionTabBar.classList.remove('tab-bar-hidden');\n }\n };\n tabVisibilityObserver?.disconnect();\n tabVisibilityObserver = new MutationObserver(keepTabsVisible);\n tabVisibilityObserver.observe(ionTabBar, { attributes: true, attributeFilter: ['class'] });\n keepTabsVisible();\n }\n resumeNative ??= await suspendNativeUIShell([\n ...(verticalSearch ? [] : [ionTabBar]),\n ionFooter,\n ionFabButton.closest('ion-fab') ?? ionFabButton,\n ]);\n try {\n searchableEventCache = await enterEvent(event, ionTabBar, ionFabButton, ionFooter, verticalSearch);\n } catch (error) {\n tabVisibilityObserver?.disconnect();\n resumeNative();\n resumeNative = undefined;\n throw error;\n }\n } else if (searchableEventCache !== undefined) {\n try {\n await leaveEvent(event, searchableEventCache, ionTabBar, ionFabButton, ionFooter, verticalSearch);\n searchableEventCache = undefined;\n } finally {\n tabVisibilityObserver?.disconnect();\n resumeNative?.();\n resumeNative = undefined;\n }\n } else {\n throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');\n }\n };\n};\n\nconst enterEvent = async (\n event: Event,\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n verticalSearch: boolean,\n): Promise<SearchableEventCache> => {\n if (!(event.target as HTMLElement)?.closest('ion-fab-button')) {\n throw throwErrorByFailedClickElement('ion-fab-button');\n }\n\n const references = getElementReferences(ionTabBar, ionFooter);\n const sizes = getElementSizes(ionTabBar, ionFabButton, references);\n const colorSelected = references.selectedTabButton\n ? getComputedStyle(references.selectedTabButton).getPropertyValue('--color-selected').trim()\n : '';\n\n if (verticalSearch) references.closeButtonIcon.setAttribute('name', 'close');\n const tabAnimations = verticalSearch\n ? []\n : [createTabBarAnimation(ionTabBar, references, sizes), createEffectAnimation(references, sizes)];\n const searchContainerAnimation = createSearchContainerAnimation(references, sizes);\n const closeButtonsAnimation = createCloseButtonsAnimation(references);\n const fabButtonAnimation = createFabButtonAnimation(ionFabButton);\n\n await createAnimation()\n .delay(ANIMATION_DELAY_BASE)\n .duration(ANIMATION_DURATION)\n .easing(ANIMATION_EASING)\n .addElement(ionFooter)\n .afterAddWrite(() => (ionFooter.style.pointerEvents = 'auto'))\n .fromTo('opacity', '0.8', '1')\n .addAnimation([...tabAnimations, fabButtonAnimation, searchContainerAnimation, closeButtonsAnimation])\n .play();\n\n return {\n elementSizes: sizes,\n colorSelected,\n };\n};\n\nconst leaveEvent = async (\n event: Event,\n searchableEventCache: SearchableEventCache,\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n verticalSearch: boolean,\n): Promise<void> => {\n if (!(event.target as HTMLElement)?.closest('ion-buttons[slot=start] ion-button')) {\n throw throwErrorByFailedClickElement('ion-buttons[slot=start] ion-button');\n }\n\n const focused = ionFooter.ownerDocument.activeElement;\n if (focused instanceof HTMLElement && ionFooter.contains(focused)) focused.blur();\n const references = getElementReferences(ionTabBar, ionFooter);\n\n const tabAnimations = verticalSearch\n ? []\n : [\n createReverseTabBarAnimation(ionTabBar, references, searchableEventCache.elementSizes),\n createReverseEffectAnimation(references, searchableEventCache.elementSizes, searchableEventCache.colorSelected),\n ];\n const searchContainerAnimation = createReverseSearchContainerAnimation(references, searchableEventCache.elementSizes);\n const closeButtonsAnimation = createReverseCloseButtonsAnimation(references);\n const fabButtonAnimation = createReverseFabButtonAnimation(ionFabButton, searchableEventCache.elementSizes);\n\n await createAnimation()\n .delay(ANIMATION_DELAY_BASE)\n .duration(ANIMATION_DURATION)\n .easing(ANIMATION_EASING)\n .addElement(ionFooter)\n .afterAddWrite(() => (ionFooter.style.pointerEvents = 'none'))\n .fromTo('opacity', '1', '0')\n .addAnimation([...tabAnimations, fabButtonAnimation, searchContainerAnimation, closeButtonsAnimation])\n .play();\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["TabBarSearchableType"],"sources":["../../src/tab-bar-searchable/index.ts"],"sourcesContent":["import { createAnimation } from '@ionic/core/components/index.js';\nimport { addSearchDismissButton } from './dismiss-button';\nimport {\n ANIMATION_DELAY_BASE,\n ANIMATION_DURATION,\n ANIMATION_EASING,\n getElementReferences,\n getElementSizes,\n throwErrorByFailedClickElement,\n type SearchableEventCache,\n type TabBarSearchableFunction,\n TabBarSearchableType,\n} from '@rdlabo/ionic-theme-utils';\nimport { isNativeUIShell, registerNativeSearch, requestNativeSearch, suspendNativeUIShell } from '../native-integration';\nimport {\n createCloseButtonsAnimation,\n createEffectAnimation,\n createFabButtonAnimation,\n createSearchContainerAnimation,\n createTabBarAnimation,\n} from './animations/enter';\nimport {\n createReverseCloseButtonsAnimation,\n createReverseEffectAnimation,\n createReverseFabButtonAnimation,\n createReverseSearchContainerAnimation,\n createReverseTabBarAnimation,\n} from './animations/leave';\n\nexport {\n TabBarSearchableType,\n type TabBarSearchableFunction,\n type SearchableEventCache,\n type ElementSizes,\n type ElementReferences,\n} from '@rdlabo/ionic-theme-utils';\n\n/**\n * <ion-fab vertical=\"bottom\" horizontal=\"end\" slot=\"fixed\">\n * <ion-fab-button (click)=\"present($event)\">\n * <ion-icon name=\"search\"></ion-icon>\n * </ion-fab-button>\n * </ion-fab>\n * <ion-footer [translucent]=\"true\">\n * <ion-toolbar>\n * <ion-buttons slot=\"start\">\n * <!-- ion-icon does not need `name` attribute. -->\n * <ion-button fill=\"default\"><ion-icon slot=\"icon-only\"></ion-icon>\n * </ion-button>\n * </ion-buttons>\n * <!-- User set `ionChange` or other events. -->\n * <ion-searchbar aria-label=\"Search\" (ionChange)=\"example($event)\"></ion-searchbar>\n * </ion-toolbar>\n * </ion-footer>\n **/\n\nexport const attachTabBarSearchable = (\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n): TabBarSearchableFunction => {\n if (!ionTabBar || !ionFabButton || !ionFooter) {\n throw new Error('TabBarSearchable should be defined with props');\n }\n // Initialize\n ionFooter.classList.add('ios-theme-searchable');\n ionFabButton.closest('ion-fab')?.classList.add('ios-theme-searchable-trigger');\n addSearchDismissButton(ionFooter);\n ionFooter.style.pointerEvents = 'none';\n ionFooter.style.opacity = '0';\n // Search dismissal belongs beside the Web search field, never in the vertical rail.\n ionFooter.querySelector('ion-buttons[slot=start]')?.classList.add('ios-theme-horizontal-only');\n const nativeSearch = registerNativeSearch(ionTabBar, ionFabButton, ionFooter);\n\n // Saved Params\n let searchableEventCache: SearchableEventCache | undefined;\n let resumeNative: (() => void) | undefined;\n // Leave is valid both while native Enter awaits its ack and after it completes.\n let nativeEntry = false;\n let nativeRequest = 0;\n let verticalSearch = false;\n let tabVisibilityObserver: MutationObserver | undefined;\n\n return async (event: Event, type: TabBarSearchableType) => {\n const selector = type === TabBarSearchableType.Enter ? 'ion-fab-button' : 'ion-buttons[slot=start] ion-button';\n if (!(event.target as HTMLElement)?.closest(selector)) throw throwErrorByFailedClickElement(selector);\n if (type === TabBarSearchableType.Leave && !nativeEntry && !searchableEventCache)\n throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');\n const hadNativeEntry = nativeEntry;\n const request = ++nativeRequest;\n if (type === TabBarSearchableType.Enter) nativeEntry ||= isNativeUIShell(ionFooter);\n const accepted = await requestNativeSearch(nativeSearch, type === TabBarSearchableType.Enter);\n if (request !== nativeRequest) return;\n if (accepted) {\n nativeEntry = type === TabBarSearchableType.Enter;\n return;\n }\n nativeEntry = false;\n if (type === TabBarSearchableType.Leave && hadNativeEntry) {\n // Forced native retirement already restored the closed Web presentation.\n return;\n }\n if (type === TabBarSearchableType.Enter) {\n verticalSearch = !!ionTabBar.closest('ion-app.ios-theme-vertical-bars');\n if (verticalSearch) {\n // Ionic hides horizontal tabs when the keyboard opens; keep this rail available.\n const keepTabsVisible = () => {\n if (ionTabBar.classList.contains('tab-bar-hidden') && ionTabBar.closest('ion-app.ios-theme-vertical-bars')) {\n ionTabBar.classList.remove('tab-bar-hidden');\n }\n };\n tabVisibilityObserver?.disconnect();\n tabVisibilityObserver = new MutationObserver(keepTabsVisible);\n tabVisibilityObserver.observe(ionTabBar, { attributes: true, attributeFilter: ['class'] });\n keepTabsVisible();\n }\n resumeNative ??= await suspendNativeUIShell([\n ...(verticalSearch ? [] : [ionTabBar]),\n ionFooter,\n ionFabButton.closest('ion-fab') ?? ionFabButton,\n ]);\n try {\n searchableEventCache = await enterEvent(event, ionTabBar, ionFabButton, ionFooter, verticalSearch);\n } catch (error) {\n tabVisibilityObserver?.disconnect();\n resumeNative();\n resumeNative = undefined;\n throw error;\n }\n } else if (searchableEventCache !== undefined) {\n try {\n await leaveEvent(event, searchableEventCache, ionTabBar, ionFabButton, ionFooter, verticalSearch);\n searchableEventCache = undefined;\n } finally {\n tabVisibilityObserver?.disconnect();\n resumeNative?.();\n resumeNative = undefined;\n }\n } else {\n throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');\n }\n };\n};\n\nconst enterEvent = async (\n event: Event,\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n verticalSearch: boolean,\n): Promise<SearchableEventCache> => {\n if (!(event.target as HTMLElement)?.closest('ion-fab-button')) {\n throw throwErrorByFailedClickElement('ion-fab-button');\n }\n\n const references = getElementReferences(ionTabBar, ionFooter);\n const sizes = getElementSizes(ionTabBar, ionFabButton, references);\n const colorSelected = references.selectedTabButton\n ? getComputedStyle(references.selectedTabButton).getPropertyValue('--color-selected').trim()\n : '';\n\n if (verticalSearch) references.closeButtonIcon.setAttribute('name', 'close');\n const tabAnimations = verticalSearch\n ? []\n : [createTabBarAnimation(ionTabBar, references, sizes), createEffectAnimation(references, sizes)];\n const searchContainerAnimation = createSearchContainerAnimation(references, sizes);\n const closeButtonsAnimation = createCloseButtonsAnimation(references);\n const fabButtonAnimation = createFabButtonAnimation(ionFabButton);\n\n await createAnimation()\n .delay(ANIMATION_DELAY_BASE)\n .duration(ANIMATION_DURATION)\n .easing(ANIMATION_EASING)\n .addElement(ionFooter)\n .afterAddWrite(() => (ionFooter.style.pointerEvents = 'auto'))\n .fromTo('opacity', '0.8', '1')\n .addAnimation([...tabAnimations, fabButtonAnimation, searchContainerAnimation, closeButtonsAnimation])\n .play();\n\n return {\n elementSizes: sizes,\n colorSelected,\n };\n};\n\nconst leaveEvent = async (\n event: Event,\n searchableEventCache: SearchableEventCache,\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n verticalSearch: boolean,\n): Promise<void> => {\n if (!(event.target as HTMLElement)?.closest('ion-buttons[slot=start] ion-button')) {\n throw throwErrorByFailedClickElement('ion-buttons[slot=start] ion-button');\n }\n\n const focused = ionFooter.ownerDocument.activeElement;\n if (focused instanceof HTMLElement && ionFooter.contains(focused)) focused.blur();\n const references = getElementReferences(ionTabBar, ionFooter);\n\n const tabAnimations = verticalSearch\n ? []\n : [\n createReverseTabBarAnimation(ionTabBar, references, searchableEventCache.elementSizes),\n createReverseEffectAnimation(references, searchableEventCache.elementSizes, searchableEventCache.colorSelected),\n ];\n const searchContainerAnimation = createReverseSearchContainerAnimation(references, searchableEventCache.elementSizes);\n const closeButtonsAnimation = createReverseCloseButtonsAnimation(references);\n const fabButtonAnimation = createReverseFabButtonAnimation(ionFabButton, searchableEventCache.elementSizes);\n\n await createAnimation()\n .delay(ANIMATION_DELAY_BASE)\n .duration(ANIMATION_DURATION)\n .easing(ANIMATION_EASING)\n .addElement(ionFooter)\n .afterAddWrite(() => (ionFooter.style.pointerEvents = 'none'))\n .fromTo('opacity', '1', '0')\n .addAnimation([...tabAnimations, fabButtonAnimation, searchContainerAnimation, closeButtonsAnimation])\n .play();\n};\n"],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Animation, NavOptions } from "@ionic/core";
|
|
1
|
+
import { Animation, NavOptions } from "@ionic/core/components";
|
|
2
2
|
//#region src/transition/index.d.ts
|
|
3
3
|
export declare const transition: (opts: TransitionOptions) => Promise<TransitionResult>;
|
|
4
4
|
export declare const lifecycle: (el: HTMLElement | undefined, eventName: string) => void;
|
package/dist/transition/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { config, raf } from "../utils.js";
|
|
2
2
|
import { createFocusController } from "../focus-controller/index.js";
|
|
3
|
-
import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from "@ionic/core";
|
|
3
|
+
import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from "@ionic/core/components/index.js";
|
|
4
4
|
import { Build, writeTask } from "@stencil/core";
|
|
5
5
|
//#region src/transition/index.ts
|
|
6
6
|
const iosTransitionAnimation = () => import("./ios.transition.js");
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/transition/index.ts"],"sourcesContent":["import { config } from '../utils';\nimport { Build, writeTask } from '@stencil/core';\n\nimport { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core';\nimport type { NavOptions, NavDirection } from '@ionic/core';\nimport type { Animation, AnimationBuilder } from '@ionic/core';\nimport { createFocusController } from '../focus-controller';\nimport { raf } from '../utils';\n\nconst iosTransitionAnimation = () => import('./ios.transition');\n// const mdTransitionAnimation = () => import('./md.transition');\nconst focusController = createFocusController();\n\n// TODO(FW-2832): types\n\n/**\n * Executes the main page transition.\n * It also manages the lifecycle of header visibility (if any)\n * to prevent visual flickering in iOS. The flickering only\n * occurs for a condensed header that is placed above the content.\n *\n * @param opts Options for the transition.\n * @returns A promise that resolves when the transition is complete.\n */\nexport const transition = (opts: TransitionOptions): Promise<TransitionResult> => {\n return new Promise((resolve, reject) => {\n writeTask(() => {\n const transitioningInactiveHeader = getIosIonHeader(opts);\n beforeTransition(opts, transitioningInactiveHeader);\n runTransition(opts)\n .then(\n (result) => {\n if (result.animation) {\n result.animation.destroy();\n }\n afterTransition(opts);\n resolve(result);\n },\n (error) => {\n afterTransition(opts);\n reject(error);\n },\n )\n .finally(() => {\n // Ensure that the header is restored to its original state.\n setHeaderTransitionClass(transitioningInactiveHeader, false);\n });\n });\n });\n};\n\nconst beforeTransition = (opts: TransitionOptions, transitioningInactiveHeader: HTMLElement | null) => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n\n focusController.saveViewFocus(leavingEl);\n\n setZIndex(enteringEl, leavingEl, opts.direction);\n // Prevent flickering of the header by adding a class.\n setHeaderTransitionClass(transitioningInactiveHeader, true);\n\n if (opts.showGoBack) {\n enteringEl.classList.add('can-go-back');\n } else {\n enteringEl.classList.remove('can-go-back');\n }\n setPageHidden(enteringEl, false);\n\n /**\n * When transitioning, the page should not\n * respond to click events. This resolves small\n * issues like users double tapping the ion-back-button.\n * These pointer events are removed in `afterTransition`.\n */\n enteringEl.style.setProperty('pointer-events', 'none');\n\n if (leavingEl) {\n setPageHidden(leavingEl, false);\n leavingEl.style.setProperty('pointer-events', 'none');\n }\n};\n\nconst runTransition = async (opts: TransitionOptions): Promise<TransitionResult> => {\n const animationBuilder = await getAnimationBuilder(opts);\n\n const ani = animationBuilder && Build.isBrowser ? animation(animationBuilder, opts) : noAnimation(opts); // fast path for no animation\n\n return ani;\n};\n\nconst afterTransition = (opts: TransitionOptions) => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n enteringEl.classList.remove('ion-page-invisible');\n enteringEl.style.removeProperty('pointer-events');\n if (leavingEl !== undefined) {\n leavingEl.classList.remove('ion-page-invisible');\n leavingEl.style.removeProperty('pointer-events');\n }\n\n focusController.setViewFocus(enteringEl);\n};\n\nconst getAnimationBuilder = async (opts: TransitionOptions): Promise<AnimationBuilder | undefined> => {\n if (!opts.leavingEl || !opts.animated || opts.duration === 0) {\n return undefined;\n }\n\n if (opts.animationBuilder) {\n return opts.animationBuilder;\n }\n\n const getAnimation =\n // opts.mode === 'ios'\n (await iosTransitionAnimation()).iosTransitionAnimation;\n // : (await mdTransitionAnimation()).mdTransitionAnimation;\n\n return getAnimation;\n};\n\nconst animation = async (animationBuilder: AnimationBuilder, opts: TransitionOptions): Promise<TransitionResult> => {\n await waitForReady(opts, true);\n\n const trans = animationBuilder(opts.baseEl, opts);\n\n fireWillEvents(opts.enteringEl, opts.leavingEl);\n\n const didComplete = await playTransition(trans, opts);\n\n if (opts.progressCallback) {\n opts.progressCallback(undefined);\n }\n\n if (didComplete) {\n fireDidEvents(opts.enteringEl, opts.leavingEl);\n }\n\n return {\n hasCompleted: didComplete,\n animation: trans,\n };\n};\n\nconst noAnimation = async (opts: TransitionOptions): Promise<TransitionResult> => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n const focusManagerEnabled = config.get('focusManagerPriority', false);\n\n /**\n * If the focus manager is enabled then we need to wait for Ionic components to be\n * rendered otherwise the component to focus may not be focused because it is hidden.\n */\n await waitForReady(opts, focusManagerEnabled);\n\n fireWillEvents(enteringEl, leavingEl);\n fireDidEvents(enteringEl, leavingEl);\n\n return {\n hasCompleted: true,\n };\n};\n\nconst waitForReady = async (opts: TransitionOptions, defaultDeep: boolean) => {\n const deep = opts.deepWait !== undefined ? opts.deepWait : defaultDeep;\n\n if (deep) {\n await Promise.all([deepReady(opts.enteringEl), deepReady(opts.leavingEl)]);\n }\n\n await notifyViewReady(opts.viewIsReady, opts.enteringEl);\n};\n\nconst notifyViewReady = async (viewIsReady: undefined | ((enteringEl: HTMLElement) => Promise<unknown>), enteringEl: HTMLElement) => {\n if (viewIsReady) {\n await viewIsReady(enteringEl);\n }\n};\n\nconst playTransition = (trans: Animation, opts: TransitionOptions): Promise<boolean> => {\n const progressCallback = opts.progressCallback;\n\n const promise = new Promise<boolean>((resolve) => {\n trans.onFinish((currentStep) => resolve(currentStep === 1));\n });\n\n // cool, let's do this, start the transition\n if (progressCallback) {\n // this is a swipe to go back, just get the transition progress ready\n // kick off the swipe animation start\n trans.progressStart(true);\n progressCallback(trans);\n } else {\n // only the top level transition should actually start \"play\"\n // kick it off and let it play through\n // ******** DOM WRITE ****************\n trans.play();\n }\n // create a callback for when the animation is done\n return promise;\n};\n\nconst fireWillEvents = (enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) => {\n lifecycle(leavingEl, LIFECYCLE_WILL_LEAVE);\n lifecycle(enteringEl, LIFECYCLE_WILL_ENTER);\n};\n\nconst fireDidEvents = (enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) => {\n lifecycle(enteringEl, LIFECYCLE_DID_ENTER);\n lifecycle(leavingEl, LIFECYCLE_DID_LEAVE);\n};\n\nexport const lifecycle = (el: HTMLElement | undefined, eventName: string) => {\n if (el) {\n const ev = new CustomEvent(eventName, {\n bubbles: false,\n cancelable: false,\n });\n el.dispatchEvent(ev);\n }\n};\n\n/**\n * Wait two request animation frame loops.\n * This allows the framework implementations enough time to mount\n * the user-defined contents. This is often needed when using inline\n * modals and popovers that accept user components. For popover,\n * the contents must be mounted for the popover to be sized correctly.\n * For modals, the contents must be mounted for iOS to run the\n * transition correctly.\n *\n * On Angular and React, a single raf is enough time, but for Vue\n * we need to wait two rafs. As a result we are using two rafs for\n * all frameworks to ensure contents are mounted.\n */\nexport const waitForMount = (): Promise<void> => {\n return new Promise((resolve) => raf(() => raf(() => resolve())));\n};\n\ntype LazyLoadedElement = Element & {\n componentOnReady?: () => Promise<unknown>;\n __registerHost?: unknown;\n};\n\nexport const deepReady = async (el: Element | undefined): Promise<void> => {\n const element = el as LazyLoadedElement | undefined;\n if (element) {\n if (element.componentOnReady != null) {\n // eslint-disable-next-line custom-rules/no-component-on-ready-method\n const stencilEl = await element.componentOnReady();\n if (stencilEl != null) {\n return;\n }\n\n /**\n * Custom elements in Stencil will have __registerHost.\n */\n } else if (element.__registerHost != null) {\n /**\n * Non-lazy loaded custom elements need to wait\n * one frame for component to be loaded.\n */\n const waitForCustomElement = new Promise((resolve) => raf(resolve));\n await waitForCustomElement;\n\n return;\n }\n await Promise.all(Array.from(element.children).map((child) => deepReady(child)));\n }\n};\n\nexport const setPageHidden = (el: HTMLElement, hidden: boolean) => {\n if (hidden) {\n el.setAttribute('aria-hidden', 'true');\n el.classList.add('ion-page-hidden');\n } else {\n el.hidden = false;\n el.removeAttribute('aria-hidden');\n el.classList.remove('ion-page-hidden');\n }\n};\n\nconst setZIndex = (enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined, direction: NavDirection | undefined) => {\n if (enteringEl !== undefined) {\n enteringEl.style.zIndex = direction === 'back' ? '99' : '101';\n }\n if (leavingEl !== undefined) {\n leavingEl.style.zIndex = '100';\n }\n};\n\n/**\n * Add a class to ensure that the header (if any)\n * does not flicker during the transition. By adding the\n * transitioning class, we ensure that the header has\n * the necessary styles to prevent the following flickers:\n * 1. When entering a page with a condensed header, the\n * header should never be visible. However,\n * it briefly renders the background color while\n * the transition is occurring.\n * 2. When leaving a page with a condensed header, the\n * header has an opacity of 0 and the pages\n * have a z-index which causes the entering page to\n * briefly show it's content underneath the leaving page.\n * 3. When entering a page or leaving a page with a fade\n * header, the header should not have a background color.\n * However, it briefly shows the background color while\n * the transition is occurring.\n *\n * @param header The header element to modify.\n * @param isTransitioning Whether the transition is occurring.\n */\nconst setHeaderTransitionClass = (header: HTMLElement | null, isTransitioning: boolean) => {\n if (!header) {\n return;\n }\n\n const transitionClass = 'header-transitioning';\n if (isTransitioning) {\n header.classList.add(transitionClass);\n } else {\n header.classList.remove(transitionClass);\n }\n};\n\nexport const getIonPageElement = (element: HTMLElement) => {\n if (element.classList.contains('ion-page')) {\n return element;\n }\n\n const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');\n if (ionPage) {\n return ionPage;\n }\n // idk, return the original element so at least something animates and we don't have a null pointer\n return element;\n};\n\n/**\n * Retrieves the ion-header element from a page based on the\n * direction of the transition.\n *\n * @param opts Options for the transition.\n * @returns The ion-header element or null if not found or not in 'ios' mode.\n */\nconst getIosIonHeader = (opts: TransitionOptions): HTMLElement | null => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n const direction = opts.direction;\n const mode = opts.mode;\n\n if (mode !== 'ios') {\n return null;\n }\n\n const element = direction === 'back' ? leavingEl : enteringEl;\n\n if (!element) {\n return null;\n }\n\n return element.querySelector('ion-header');\n};\n\nexport interface TransitionOptions extends NavOptions {\n progressCallback?: (ani: Animation | undefined) => void;\n baseEl: HTMLElement;\n enteringEl: HTMLElement;\n leavingEl: HTMLElement | undefined;\n}\n\nexport interface TransitionResult {\n hasCompleted: boolean;\n animation?: Animation;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/transition/index.ts"],"sourcesContent":["import { config } from '../utils';\nimport { Build, writeTask } from '@stencil/core';\n\nimport { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core/components/index.js';\nimport type { NavOptions, NavDirection } from '@ionic/core/components';\nimport type { Animation, AnimationBuilder } from '@ionic/core/components';\nimport { createFocusController } from '../focus-controller';\nimport { raf } from '../utils';\n\nconst iosTransitionAnimation = () => import('./ios.transition');\n// const mdTransitionAnimation = () => import('./md.transition');\nconst focusController = createFocusController();\n\n// TODO(FW-2832): types\n\n/**\n * Executes the main page transition.\n * It also manages the lifecycle of header visibility (if any)\n * to prevent visual flickering in iOS. The flickering only\n * occurs for a condensed header that is placed above the content.\n *\n * @param opts Options for the transition.\n * @returns A promise that resolves when the transition is complete.\n */\nexport const transition = (opts: TransitionOptions): Promise<TransitionResult> => {\n return new Promise((resolve, reject) => {\n writeTask(() => {\n const transitioningInactiveHeader = getIosIonHeader(opts);\n beforeTransition(opts, transitioningInactiveHeader);\n runTransition(opts)\n .then(\n (result) => {\n if (result.animation) {\n result.animation.destroy();\n }\n afterTransition(opts);\n resolve(result);\n },\n (error) => {\n afterTransition(opts);\n reject(error);\n },\n )\n .finally(() => {\n // Ensure that the header is restored to its original state.\n setHeaderTransitionClass(transitioningInactiveHeader, false);\n });\n });\n });\n};\n\nconst beforeTransition = (opts: TransitionOptions, transitioningInactiveHeader: HTMLElement | null) => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n\n focusController.saveViewFocus(leavingEl);\n\n setZIndex(enteringEl, leavingEl, opts.direction);\n // Prevent flickering of the header by adding a class.\n setHeaderTransitionClass(transitioningInactiveHeader, true);\n\n if (opts.showGoBack) {\n enteringEl.classList.add('can-go-back');\n } else {\n enteringEl.classList.remove('can-go-back');\n }\n setPageHidden(enteringEl, false);\n\n /**\n * When transitioning, the page should not\n * respond to click events. This resolves small\n * issues like users double tapping the ion-back-button.\n * These pointer events are removed in `afterTransition`.\n */\n enteringEl.style.setProperty('pointer-events', 'none');\n\n if (leavingEl) {\n setPageHidden(leavingEl, false);\n leavingEl.style.setProperty('pointer-events', 'none');\n }\n};\n\nconst runTransition = async (opts: TransitionOptions): Promise<TransitionResult> => {\n const animationBuilder = await getAnimationBuilder(opts);\n\n const ani = animationBuilder && Build.isBrowser ? animation(animationBuilder, opts) : noAnimation(opts); // fast path for no animation\n\n return ani;\n};\n\nconst afterTransition = (opts: TransitionOptions) => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n enteringEl.classList.remove('ion-page-invisible');\n enteringEl.style.removeProperty('pointer-events');\n if (leavingEl !== undefined) {\n leavingEl.classList.remove('ion-page-invisible');\n leavingEl.style.removeProperty('pointer-events');\n }\n\n focusController.setViewFocus(enteringEl);\n};\n\nconst getAnimationBuilder = async (opts: TransitionOptions): Promise<AnimationBuilder | undefined> => {\n if (!opts.leavingEl || !opts.animated || opts.duration === 0) {\n return undefined;\n }\n\n if (opts.animationBuilder) {\n return opts.animationBuilder;\n }\n\n const getAnimation =\n // opts.mode === 'ios'\n (await iosTransitionAnimation()).iosTransitionAnimation;\n // : (await mdTransitionAnimation()).mdTransitionAnimation;\n\n return getAnimation;\n};\n\nconst animation = async (animationBuilder: AnimationBuilder, opts: TransitionOptions): Promise<TransitionResult> => {\n await waitForReady(opts, true);\n\n const trans = animationBuilder(opts.baseEl, opts);\n\n fireWillEvents(opts.enteringEl, opts.leavingEl);\n\n const didComplete = await playTransition(trans, opts);\n\n if (opts.progressCallback) {\n opts.progressCallback(undefined);\n }\n\n if (didComplete) {\n fireDidEvents(opts.enteringEl, opts.leavingEl);\n }\n\n return {\n hasCompleted: didComplete,\n animation: trans,\n };\n};\n\nconst noAnimation = async (opts: TransitionOptions): Promise<TransitionResult> => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n const focusManagerEnabled = config.get('focusManagerPriority', false);\n\n /**\n * If the focus manager is enabled then we need to wait for Ionic components to be\n * rendered otherwise the component to focus may not be focused because it is hidden.\n */\n await waitForReady(opts, focusManagerEnabled);\n\n fireWillEvents(enteringEl, leavingEl);\n fireDidEvents(enteringEl, leavingEl);\n\n return {\n hasCompleted: true,\n };\n};\n\nconst waitForReady = async (opts: TransitionOptions, defaultDeep: boolean) => {\n const deep = opts.deepWait !== undefined ? opts.deepWait : defaultDeep;\n\n if (deep) {\n await Promise.all([deepReady(opts.enteringEl), deepReady(opts.leavingEl)]);\n }\n\n await notifyViewReady(opts.viewIsReady, opts.enteringEl);\n};\n\nconst notifyViewReady = async (viewIsReady: undefined | ((enteringEl: HTMLElement) => Promise<unknown>), enteringEl: HTMLElement) => {\n if (viewIsReady) {\n await viewIsReady(enteringEl);\n }\n};\n\nconst playTransition = (trans: Animation, opts: TransitionOptions): Promise<boolean> => {\n const progressCallback = opts.progressCallback;\n\n const promise = new Promise<boolean>((resolve) => {\n trans.onFinish((currentStep) => resolve(currentStep === 1));\n });\n\n // cool, let's do this, start the transition\n if (progressCallback) {\n // this is a swipe to go back, just get the transition progress ready\n // kick off the swipe animation start\n trans.progressStart(true);\n progressCallback(trans);\n } else {\n // only the top level transition should actually start \"play\"\n // kick it off and let it play through\n // ******** DOM WRITE ****************\n trans.play();\n }\n // create a callback for when the animation is done\n return promise;\n};\n\nconst fireWillEvents = (enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) => {\n lifecycle(leavingEl, LIFECYCLE_WILL_LEAVE);\n lifecycle(enteringEl, LIFECYCLE_WILL_ENTER);\n};\n\nconst fireDidEvents = (enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined) => {\n lifecycle(enteringEl, LIFECYCLE_DID_ENTER);\n lifecycle(leavingEl, LIFECYCLE_DID_LEAVE);\n};\n\nexport const lifecycle = (el: HTMLElement | undefined, eventName: string) => {\n if (el) {\n const ev = new CustomEvent(eventName, {\n bubbles: false,\n cancelable: false,\n });\n el.dispatchEvent(ev);\n }\n};\n\n/**\n * Wait two request animation frame loops.\n * This allows the framework implementations enough time to mount\n * the user-defined contents. This is often needed when using inline\n * modals and popovers that accept user components. For popover,\n * the contents must be mounted for the popover to be sized correctly.\n * For modals, the contents must be mounted for iOS to run the\n * transition correctly.\n *\n * On Angular and React, a single raf is enough time, but for Vue\n * we need to wait two rafs. As a result we are using two rafs for\n * all frameworks to ensure contents are mounted.\n */\nexport const waitForMount = (): Promise<void> => {\n return new Promise((resolve) => raf(() => raf(() => resolve())));\n};\n\ntype LazyLoadedElement = Element & {\n componentOnReady?: () => Promise<unknown>;\n __registerHost?: unknown;\n};\n\nexport const deepReady = async (el: Element | undefined): Promise<void> => {\n const element = el as LazyLoadedElement | undefined;\n if (element) {\n if (element.componentOnReady != null) {\n // eslint-disable-next-line custom-rules/no-component-on-ready-method\n const stencilEl = await element.componentOnReady();\n if (stencilEl != null) {\n return;\n }\n\n /**\n * Custom elements in Stencil will have __registerHost.\n */\n } else if (element.__registerHost != null) {\n /**\n * Non-lazy loaded custom elements need to wait\n * one frame for component to be loaded.\n */\n const waitForCustomElement = new Promise((resolve) => raf(resolve));\n await waitForCustomElement;\n\n return;\n }\n await Promise.all(Array.from(element.children).map((child) => deepReady(child)));\n }\n};\n\nexport const setPageHidden = (el: HTMLElement, hidden: boolean) => {\n if (hidden) {\n el.setAttribute('aria-hidden', 'true');\n el.classList.add('ion-page-hidden');\n } else {\n el.hidden = false;\n el.removeAttribute('aria-hidden');\n el.classList.remove('ion-page-hidden');\n }\n};\n\nconst setZIndex = (enteringEl: HTMLElement | undefined, leavingEl: HTMLElement | undefined, direction: NavDirection | undefined) => {\n if (enteringEl !== undefined) {\n enteringEl.style.zIndex = direction === 'back' ? '99' : '101';\n }\n if (leavingEl !== undefined) {\n leavingEl.style.zIndex = '100';\n }\n};\n\n/**\n * Add a class to ensure that the header (if any)\n * does not flicker during the transition. By adding the\n * transitioning class, we ensure that the header has\n * the necessary styles to prevent the following flickers:\n * 1. When entering a page with a condensed header, the\n * header should never be visible. However,\n * it briefly renders the background color while\n * the transition is occurring.\n * 2. When leaving a page with a condensed header, the\n * header has an opacity of 0 and the pages\n * have a z-index which causes the entering page to\n * briefly show it's content underneath the leaving page.\n * 3. When entering a page or leaving a page with a fade\n * header, the header should not have a background color.\n * However, it briefly shows the background color while\n * the transition is occurring.\n *\n * @param header The header element to modify.\n * @param isTransitioning Whether the transition is occurring.\n */\nconst setHeaderTransitionClass = (header: HTMLElement | null, isTransitioning: boolean) => {\n if (!header) {\n return;\n }\n\n const transitionClass = 'header-transitioning';\n if (isTransitioning) {\n header.classList.add(transitionClass);\n } else {\n header.classList.remove(transitionClass);\n }\n};\n\nexport const getIonPageElement = (element: HTMLElement) => {\n if (element.classList.contains('ion-page')) {\n return element;\n }\n\n const ionPage = element.querySelector(':scope > .ion-page, :scope > ion-nav, :scope > ion-tabs');\n if (ionPage) {\n return ionPage;\n }\n // idk, return the original element so at least something animates and we don't have a null pointer\n return element;\n};\n\n/**\n * Retrieves the ion-header element from a page based on the\n * direction of the transition.\n *\n * @param opts Options for the transition.\n * @returns The ion-header element or null if not found or not in 'ios' mode.\n */\nconst getIosIonHeader = (opts: TransitionOptions): HTMLElement | null => {\n const enteringEl = opts.enteringEl;\n const leavingEl = opts.leavingEl;\n const direction = opts.direction;\n const mode = opts.mode;\n\n if (mode !== 'ios') {\n return null;\n }\n\n const element = direction === 'back' ? leavingEl : enteringEl;\n\n if (!element) {\n return null;\n }\n\n return element.querySelector('ion-header');\n};\n\nexport interface TransitionOptions extends NavOptions {\n progressCallback?: (ani: Animation | undefined) => void;\n baseEl: HTMLElement;\n enteringEl: HTMLElement;\n leavingEl: HTMLElement | undefined;\n}\n\nexport interface TransitionResult {\n hasCompleted: boolean;\n animation?: Animation;\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { TransitionOptions } from "./index.js";
|
|
2
|
-
import { Animation } from "@ionic/core";
|
|
3
2
|
import { shadow } from "@rdlabo/ionic-theme-utils";
|
|
3
|
+
import { Animation } from "@ionic/core/components";
|
|
4
4
|
//#region src/transition/ios.transition.d.ts
|
|
5
5
|
export interface IosTransitionConfig {
|
|
6
6
|
radius: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ios.transition.js","names":[],"sources":["../../src/transition/ios.transition.ts"],"sourcesContent":["import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';\nimport type { Animation } from '@ionic/core';\nimport type { TransitionOptions } from './index';\nimport { getIonPageElement } from './index';\nimport { withNativeUIShellTransition } from '../native-integration/transition';\n\nexport { shadow };\n\nexport interface IosTransitionConfig {\n radius: number;\n}\n\nconst transitionConfig = {\n offLeftPercent: 30,\n getIonPageElement,\n shouldAnimateFixedBackButton: (navEl: HTMLElement) => !navEl.closest('ion-app.ios-theme-vertical-bars'),\n radius: 0,\n};\n\nexport const setConfig = (config: Partial<IosTransitionConfig>): void => {\n Object.assign(transitionConfig, config);\n};\n\nexport const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation = withNativeUIShellTransition(\n createIosTransitionAnimation<TransitionOptions>(transitionConfig),\n);\n"],"mappings":";;;;AAYA,MAAM,mBAAmB;CACvB,gBAAgB;CAChB;CACA,+BAA+B,UAAuB,CAAC,MAAM,QAAQ,iCAAiC;CACtG,QAAQ;AACV;AAEA,MAAa,aAAa,WAA+C;CACvE,OAAO,OAAO,kBAAkB,MAAM;AACxC;AAEA,MAAa,yBAAqF,4BAChG,6BAAgD,gBAAgB,CAClE"}
|
|
1
|
+
{"version":3,"file":"ios.transition.js","names":[],"sources":["../../src/transition/ios.transition.ts"],"sourcesContent":["import { createIosTransitionAnimation, shadow } from '@rdlabo/ionic-theme-utils';\nimport type { Animation } from '@ionic/core/components';\nimport type { TransitionOptions } from './index';\nimport { getIonPageElement } from './index';\nimport { withNativeUIShellTransition } from '../native-integration/transition';\n\nexport { shadow };\n\nexport interface IosTransitionConfig {\n radius: number;\n}\n\nconst transitionConfig = {\n offLeftPercent: 30,\n getIonPageElement,\n shouldAnimateFixedBackButton: (navEl: HTMLElement) => !navEl.closest('ion-app.ios-theme-vertical-bars'),\n radius: 0,\n};\n\nexport const setConfig = (config: Partial<IosTransitionConfig>): void => {\n Object.assign(transitionConfig, config);\n};\n\nexport const iosTransitionAnimation: (navEl: HTMLElement, opts: TransitionOptions) => Animation = withNativeUIShellTransition(\n createIosTransitionAnimation<TransitionOptions>(transitionConfig),\n);\n"],"mappings":";;;;AAYA,MAAM,mBAAmB;CACvB,gBAAgB;CAChB;CACA,+BAA+B,UAAuB,CAAC,MAAM,QAAQ,iCAAiC;CACtG,QAAQ;AACV;AAEA,MAAa,aAAa,WAA+C;CACvE,OAAO,OAAO,kBAAkB,MAAM;AACxC;AAEA,MAAa,yBAAqF,4BAChG,6BAAgD,gBAAgB,CAClE"}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AnimationPosition } from "./sheets-of-glass/interfaces.js";
|
|
2
|
-
import { IonicConfig } from "@ionic/core";
|
|
2
|
+
import { IonicConfig } from "@ionic/core/components";
|
|
3
3
|
//#region src/utils.d.ts
|
|
4
4
|
export declare const getElementRoot: (el: HTMLElement, fallback?: HTMLElement) => HTMLElement | ShadowRoot;
|
|
5
5
|
export declare const raf: (h: FrameRequestCallback) => number;
|
package/dist/utils.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import { AnimationPosition } from './sheets-of-glass/interfaces';\nimport { IonicConfig } from '@ionic/core';\n\ndeclare const __zone_symbol__requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;\ndeclare const requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;\n\nexport const getElementRoot = (el: HTMLElement, fallback: HTMLElement = el) => {\n return el.shadowRoot || fallback;\n};\n\nexport const raf = (h: FrameRequestCallback) => {\n if (typeof __zone_symbol__requestAnimationFrame === 'function') {\n return __zone_symbol__requestAnimationFrame(h);\n }\n if (typeof requestAnimationFrame === 'function') {\n return requestAnimationFrame(h);\n }\n return setTimeout(h);\n};\n\nexport const cloneElement = (tagName: string, useCache: boolean = true): HTMLElement => {\n if (useCache) {\n const cachedElement = document.querySelector(`${tagName}.ion-cloned-element`);\n if (cachedElement !== null) {\n return cachedElement as HTMLElement;\n }\n }\n\n const clonedEl = document.createElement(tagName) as HTMLElement;\n clonedEl.classList.add('ion-cloned-element');\n clonedEl.style.setProperty('display', 'none');\n document.body.appendChild(clonedEl);\n\n return clonedEl;\n};\n\nexport const getStep = (targetX: number, animationPosition: AnimationPosition) => {\n if (animationPosition === undefined) {\n return 0;\n }\n const currentX = targetX - animationPosition.width / 2;\n let progress = (currentX - animationPosition.minPositionX) / (animationPosition.maxPositionX - animationPosition.minPositionX);\n progress = Math.max(0, Math.min(1, progress)); // clamp 0〜1\n return progress;\n};\n\nexport const changeSelectedElement = (\n targetElement: HTMLElement,\n selectedElement: HTMLElement,\n effectTagName: string,\n selectedClassName: string,\n): void => {\n targetElement.querySelectorAll(effectTagName).forEach((element) => {\n element.classList.remove(selectedClassName);\n element.classList.remove('ion-activated');\n });\n selectedElement.classList.add('ion-activated');\n};\n\nexport class Config {\n private m = new Map<keyof IonicConfig, unknown>();\n\n reset(configObj: IonicConfig) {\n this.m = new Map<keyof IonicConfig, unknown>(Object.entries(configObj) as [keyof IonicConfig, unknown][]);\n }\n\n get<T>(key: keyof IonicConfig, fallback?: T): T {\n const value = this.m.get(key);\n return value !== undefined ? (value as T) : (fallback as T);\n }\n\n getBoolean(key: keyof IonicConfig, fallback = false): boolean {\n const val = this.m.get(key);\n if (val === undefined) {\n return fallback;\n }\n if (typeof val === 'string') {\n return val === 'true';\n }\n return !!val;\n }\n\n getNumber(key: keyof IonicConfig, fallback?: number): number {\n const val = parseFloat(String(this.m.get(key)));\n return isNaN(val) ? (fallback !== undefined ? fallback : NaN) : val;\n }\n\n set(key: keyof IonicConfig, value: unknown) {\n this.m.set(key, value);\n }\n}\n\nexport const config = /*@__PURE__*/ new Config();\n"],"mappings":";AAMA,MAAa,kBAAkB,IAAiB,WAAwB,OAAO;CAC7E,OAAO,GAAG,cAAc;AAC1B;AAEA,MAAa,OAAO,MAA4B;CAC9C,IAAI,OAAO,yCAAyC,YAClD,OAAO,qCAAqC,CAAC;CAE/C,IAAI,OAAO,0BAA0B,YACnC,OAAO,sBAAsB,CAAC;CAEhC,OAAO,WAAW,CAAC;AACrB;AAEA,MAAa,gBAAgB,SAAiB,WAAoB,SAAsB;CACtF,IAAI,UAAU;EACZ,MAAM,gBAAgB,SAAS,cAAc,GAAG,QAAQ,oBAAoB;EAC5E,IAAI,kBAAkB,MACpB,OAAO;CAEX;CAEA,MAAM,WAAW,SAAS,cAAc,OAAO;CAC/C,SAAS,UAAU,IAAI,oBAAoB;CAC3C,SAAS,MAAM,YAAY,WAAW,MAAM;CAC5C,SAAS,KAAK,YAAY,QAAQ;CAElC,OAAO;AACT;AAEA,MAAa,WAAW,SAAiB,sBAAyC;CAChF,IAAI,sBAAsB,KAAA,GACxB,OAAO;CAGT,IAAI,YADa,UAAU,kBAAkB,QAAQ,IAC1B,kBAAkB,iBAAiB,kBAAkB,eAAe,kBAAkB;CACjH,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC;CAC5C,OAAO;AACT;AAEA,MAAa,yBACX,eACA,iBACA,eACA,sBACS;CACT,cAAc,iBAAiB,aAAa,CAAC,CAAC,SAAS,YAAY;EACjE,QAAQ,UAAU,OAAO,iBAAiB;EAC1C,QAAQ,UAAU,OAAO,eAAe;CAC1C,CAAC;CACD,gBAAgB,UAAU,IAAI,eAAe;AAC/C;AAEA,IAAa,SAAb,MAAoB;;EAClB,KAAQ,oBAAI,IAAI,IAAgC;;CAEhD,MAAM,WAAwB;EAC5B,KAAK,IAAI,IAAI,IAAgC,OAAO,QAAQ,SAAS,CAAmC;CAC1G;CAEA,IAAO,KAAwB,UAAiB;EAC9C,MAAM,QAAQ,KAAK,EAAE,IAAI,GAAG;EAC5B,OAAO,UAAU,KAAA,IAAa,QAAe;CAC/C;CAEA,WAAW,KAAwB,WAAW,OAAgB;EAC5D,MAAM,MAAM,KAAK,EAAE,IAAI,GAAG;EAC1B,IAAI,QAAQ,KAAA,GACV,OAAO;EAET,IAAI,OAAO,QAAQ,UACjB,OAAO,QAAQ;EAEjB,OAAO,CAAC,CAAC;CACX;CAEA,UAAU,KAAwB,UAA2B;EAC3D,MAAM,MAAM,WAAW,OAAO,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC;EAC9C,OAAO,MAAM,GAAG,IAAK,aAAa,KAAA,IAAY,WAAW,MAAO;CAClE;CAEA,IAAI,KAAwB,OAAgB;EAC1C,KAAK,EAAE,IAAI,KAAK,KAAK;CACvB;AACF;AAEA,MAAa,uBAAuB,IAAI,OAAO"}
|
|
1
|
+
{"version":3,"file":"utils.js","names":[],"sources":["../src/utils.ts"],"sourcesContent":["import { AnimationPosition } from './sheets-of-glass/interfaces';\nimport type { IonicConfig } from '@ionic/core/components';\n\ndeclare const __zone_symbol__requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;\ndeclare const requestAnimationFrame: ((callback: FrameRequestCallback) => number) | undefined;\n\nexport const getElementRoot = (el: HTMLElement, fallback: HTMLElement = el) => {\n return el.shadowRoot || fallback;\n};\n\nexport const raf = (h: FrameRequestCallback) => {\n if (typeof __zone_symbol__requestAnimationFrame === 'function') {\n return __zone_symbol__requestAnimationFrame(h);\n }\n if (typeof requestAnimationFrame === 'function') {\n return requestAnimationFrame(h);\n }\n return setTimeout(h);\n};\n\nexport const cloneElement = (tagName: string, useCache: boolean = true): HTMLElement => {\n if (useCache) {\n const cachedElement = document.querySelector(`${tagName}.ion-cloned-element`);\n if (cachedElement !== null) {\n return cachedElement as HTMLElement;\n }\n }\n\n const clonedEl = document.createElement(tagName) as HTMLElement;\n clonedEl.classList.add('ion-cloned-element');\n clonedEl.style.setProperty('display', 'none');\n document.body.appendChild(clonedEl);\n\n return clonedEl;\n};\n\nexport const getStep = (targetX: number, animationPosition: AnimationPosition) => {\n if (animationPosition === undefined) {\n return 0;\n }\n const currentX = targetX - animationPosition.width / 2;\n let progress = (currentX - animationPosition.minPositionX) / (animationPosition.maxPositionX - animationPosition.minPositionX);\n progress = Math.max(0, Math.min(1, progress)); // clamp 0〜1\n return progress;\n};\n\nexport const changeSelectedElement = (\n targetElement: HTMLElement,\n selectedElement: HTMLElement,\n effectTagName: string,\n selectedClassName: string,\n): void => {\n targetElement.querySelectorAll(effectTagName).forEach((element) => {\n element.classList.remove(selectedClassName);\n element.classList.remove('ion-activated');\n });\n selectedElement.classList.add('ion-activated');\n};\n\nexport class Config {\n private m = new Map<keyof IonicConfig, unknown>();\n\n reset(configObj: IonicConfig) {\n this.m = new Map<keyof IonicConfig, unknown>(Object.entries(configObj) as [keyof IonicConfig, unknown][]);\n }\n\n get<T>(key: keyof IonicConfig, fallback?: T): T {\n const value = this.m.get(key);\n return value !== undefined ? (value as T) : (fallback as T);\n }\n\n getBoolean(key: keyof IonicConfig, fallback = false): boolean {\n const val = this.m.get(key);\n if (val === undefined) {\n return fallback;\n }\n if (typeof val === 'string') {\n return val === 'true';\n }\n return !!val;\n }\n\n getNumber(key: keyof IonicConfig, fallback?: number): number {\n const val = parseFloat(String(this.m.get(key)));\n return isNaN(val) ? (fallback !== undefined ? fallback : NaN) : val;\n }\n\n set(key: keyof IonicConfig, value: unknown) {\n this.m.set(key, value);\n }\n}\n\nexport const config = /*@__PURE__*/ new Config();\n"],"mappings":";AAMA,MAAa,kBAAkB,IAAiB,WAAwB,OAAO;CAC7E,OAAO,GAAG,cAAc;AAC1B;AAEA,MAAa,OAAO,MAA4B;CAC9C,IAAI,OAAO,yCAAyC,YAClD,OAAO,qCAAqC,CAAC;CAE/C,IAAI,OAAO,0BAA0B,YACnC,OAAO,sBAAsB,CAAC;CAEhC,OAAO,WAAW,CAAC;AACrB;AAEA,MAAa,gBAAgB,SAAiB,WAAoB,SAAsB;CACtF,IAAI,UAAU;EACZ,MAAM,gBAAgB,SAAS,cAAc,GAAG,QAAQ,oBAAoB;EAC5E,IAAI,kBAAkB,MACpB,OAAO;CAEX;CAEA,MAAM,WAAW,SAAS,cAAc,OAAO;CAC/C,SAAS,UAAU,IAAI,oBAAoB;CAC3C,SAAS,MAAM,YAAY,WAAW,MAAM;CAC5C,SAAS,KAAK,YAAY,QAAQ;CAElC,OAAO;AACT;AAEA,MAAa,WAAW,SAAiB,sBAAyC;CAChF,IAAI,sBAAsB,KAAA,GACxB,OAAO;CAGT,IAAI,YADa,UAAU,kBAAkB,QAAQ,IAC1B,kBAAkB,iBAAiB,kBAAkB,eAAe,kBAAkB;CACjH,WAAW,KAAK,IAAI,GAAG,KAAK,IAAI,GAAG,QAAQ,CAAC;CAC5C,OAAO;AACT;AAEA,MAAa,yBACX,eACA,iBACA,eACA,sBACS;CACT,cAAc,iBAAiB,aAAa,CAAC,CAAC,SAAS,YAAY;EACjE,QAAQ,UAAU,OAAO,iBAAiB;EAC1C,QAAQ,UAAU,OAAO,eAAe;CAC1C,CAAC;CACD,gBAAgB,UAAU,IAAI,eAAe;AAC/C;AAEA,IAAa,SAAb,MAAoB;;EAClB,KAAQ,oBAAI,IAAI,IAAgC;;CAEhD,MAAM,WAAwB;EAC5B,KAAK,IAAI,IAAI,IAAgC,OAAO,QAAQ,SAAS,CAAmC;CAC1G;CAEA,IAAO,KAAwB,UAAiB;EAC9C,MAAM,QAAQ,KAAK,EAAE,IAAI,GAAG;EAC5B,OAAO,UAAU,KAAA,IAAa,QAAe;CAC/C;CAEA,WAAW,KAAwB,WAAW,OAAgB;EAC5D,MAAM,MAAM,KAAK,EAAE,IAAI,GAAG;EAC1B,IAAI,QAAQ,KAAA,GACV,OAAO;EAET,IAAI,OAAO,QAAQ,UACjB,OAAO,QAAQ;EAEjB,OAAO,CAAC,CAAC;CACX;CAEA,UAAU,KAAwB,UAA2B;EAC3D,MAAM,MAAM,WAAW,OAAO,KAAK,EAAE,IAAI,GAAG,CAAC,CAAC;EAC9C,OAAO,MAAM,GAAG,IAAK,aAAa,KAAA,IAAY,WAAW,MAAO;CAClE;CAEA,IAAI,KAAwB,OAAgB;EAC1C,KAAK,EAAE,IAAI,KAAK,KAAK;CACvB;AACF;AAEA,MAAa,uBAAuB,IAAI,OAAO"}
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -98,9 +98,9 @@ With vertical bars, the native rail keeps every tab and its selection. SwiftUI
|
|
|
98
98
|
|
|
99
99
|
Search registration does not bypass placement restrictions. Its searchbar and close button must be in fixed footer toolbars. Its trigger must belong to an `ion-fab[slot="fixed"]` directly inside `ion-content`, or directly on the existing non-scrolling `.ion-page` layout. A wrapper inside scrolling content is not a fixed slot.
|
|
100
100
|
|
|
101
|
-
For horizontal tabs, while a registration is alive, Native UI Shell keeps the searchable controller even when the page is transitioning or temporarily unavailable (`available: false`)
|
|
101
|
+
For horizontal tabs, while a registration is alive, Native UI Shell keeps the searchable controller even when the page is transitioning or temporarily unavailable (`available: false`). The same controller manages resting tabs and active search. When resting, its view is fitted to the source `ion-tab-bar` to preserve the ordinary tab platter’s source width. UIKit can limit the platter width for small tab groups. UIKit owns the search button's placement beside the tabs. Register before the destination page finishes entering (for example in `ionViewWillEnter`) so the first visit does not paint ordinary tabs and then swap.
|
|
102
102
|
|
|
103
|
-
|
|
103
|
+
Opening search preserves the selected Ionic tab and does not automatically show the keyboard (`automaticallyActivatesSearch` stays off). Tap the field or call `ion-searchbar.setFocus()` for the keyboard. While search is active, Native UI Shell freezes Web layout projection and holds Capacitor Keyboard resize at `none`; UIKit owns tab/search chrome without frame fitting during the session. Closing search restores the ordinary tabs to their source placement. Ordinary native tabs keep an optimistic selection until the Web `selected` state catches up. Input events and application `value` updates continue across the bridge until search closes. The trigger's resolved SVG, accessibility label and the search icon are projected from Ionic, including `ion-icon name`.
|
|
104
104
|
|
|
105
105
|
Native edits pass through Ionic's input handlers, preserving `ionInput` debounce, `ionChange`, `ionFocus`, `ionBlur`, and `ionClear`. Programmatic `value` changes do not emit `ionInput`; synchronous application corrections and stale native input are distinguished. Native editing owns marked text and the caret. Returning through the footer's close action retains the value and does not emit `ionCancel` or `ionClear`.
|
|
106
106
|
|
|
@@ -126,7 +126,7 @@ The built-in `iosTransitionAnimation` waits for native retirement before startin
|
|
|
126
126
|
|
|
127
127
|
Tab switches skip the Web/native crossfade so a retiring UIKit snapshot cannot linger over the next tab. Detection uses the router URL versus the still-selected tab on `ionViewWillLeave` (and vanilla `ionTabsWillChange` / `ionTabsDidChange` DOM events). Stack pushes and pops keep the normal 180ms handoff.
|
|
128
128
|
|
|
129
|
-
Standard Ionic overlays suspend native projection until dismissal. Unsupported searchable-tab configurations use the existing Web animation and share this ownership with Web glass gestures. CSS motion of supported containing surfaces also causes temporary Web rendering.
|
|
129
|
+
Standard Ionic overlays suspend native projection until dismissal, except for the opt-in native Vertical Bars popovers described below. Unsupported searchable-tab configurations use the existing Web animation and share this ownership with Web glass gestures. CSS motion of supported containing surfaces also causes temporary Web rendering.
|
|
130
130
|
|
|
131
131
|
During retirement, the source is restored and allowed to paint before its native cover is removed. During acquisition, the source is hidden only after a successful, current native response. Delayed responses are revalidated per control: existing eligible controls retain their native cover while content updates catch up. Only removed or ineligible sources return to Web; newly acquired sources require an exact acknowledgement. Ordinary page mutations never call the global clear operation. UIKit tab instances and items are retained, and equal frames/selections are not reapplied. Duplicate/stale activations are discarded. WebKit and UIKit still render separately: the implementation avoids an intentional blank frame, but does not provide an OS-level atomic compositing guarantee. Validate custom transitions and overlays on the app's supported simulators before rollout; unknown overlay systems are outside the automatic integration contract.
|
|
132
132
|
|
|
@@ -154,7 +154,23 @@ const shell = await enableNativeUIShell({
|
|
|
154
154
|
const disabledShell = await enableNativeUIShell({ enabled: false });
|
|
155
155
|
```
|
|
156
156
|
|
|
157
|
-
Omitting `controls` enables
|
|
157
|
+
Omitting `controls` enables `tabs`, `toolbar`, `segment`, and `fab` for backward compatibility. When `controls` is present, only entries set to `true` are native-eligible. `popover` is opt-in and defaults to `false`.
|
|
158
|
+
|
|
159
|
+
### Popovers anchored to Vertical Bars
|
|
160
|
+
|
|
161
|
+
Enable `controls.popover` together with `controls.toolbar` to present popovers from accepted native Vertical Bars `ion-button` controls:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
await enableNativeUIShell({
|
|
165
|
+
controls: { tabs: true, toolbar: true, segment: true, fab: true, popover: true },
|
|
166
|
+
});
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The same option is available through `enableVerticalControlArea()`. Use an `ion-popover` whose `trigger` identifies the projected button, or pass that button's click event to `PopoverController.create({ event, ... })`. The button must be accepted by native Vertical Bars; regular toolbar buttons, body controls, and missing or rejected anchors retain Ionic's Web presentation.
|
|
170
|
+
|
|
171
|
+
The page's native controls stay projected throughout preparation, presentation and dismissal. If the same native anchor accepts a tap while Ionic is still dismissing, one activation is retained and revalidated after cleanup; repeated taps coalesce. The relay prepares its child WebView while Ionic initializes the content, bypasses the Web enter animation, and presents through the button's standard SwiftUI popover. It does not resize or reparent the source WebView. Ionic retains its controller, lifecycle, live application handlers and dismissal data; the live content temporarily moves into the child document and is restored during cleanup.
|
|
172
|
+
|
|
173
|
+
Only one popover is relayed at a time. Opening another overlay or menu, leaving the page, suspending the shell, or losing the native layout restores Web rendering. Modal, sheet and alert relay are outside this feature. Native presentation owns the arrow, corners and animation; custom Web enter/leave animations do not replace them. Initial content dimensions come from Ionic; arbitrary cross-document CSS, document-bound queries and full VoiceOver/hardware-keyboard parity are not guaranteed. Failure to prepare or find the native anchor restores the Web popover.
|
|
158
174
|
|
|
159
175
|
For a custom modal or overlay that Native UI Shell cannot detect, acquire a suspension before presenting it. The resolved suspension means projected controls have returned to Web rendering. Always release it after dismissal:
|
|
160
176
|
|
package/docs/vertical-bars.md
CHANGED
|
@@ -85,6 +85,8 @@ All fills (`default`, `clear`, `solid`, and `outline`) and Ionic colors follow t
|
|
|
85
85
|
|
|
86
86
|
The rule applies to individual buttons and buttons inside `ion-buttons`, on ordinary pages and in the topmost full-width modal. Centered modals, menus, and popovers keep their own toolbar layout. Add `.ios-theme-horizontal-only` to a group or individual button to keep it horizontal. Placement is chosen when a routed page enters; changing an existing button's content or icon slot does not move it between the toolbar and rail until the page leaves and re-enters.
|
|
87
87
|
|
|
88
|
+
A toolbar whose content has all moved into the rail collapses while projection is active. Toolbars containing a title, direct text, other content or a horizontal-only control remain visible. Removing projection restores the original toolbar.
|
|
89
|
+
|
|
88
90
|
### Choose button appearance
|
|
89
91
|
|
|
90
92
|
`buttonProjection` and the local projection settings below are available in `1.2.0`.
|
|
@@ -100,7 +102,7 @@ For native vertical `ion-button` and `ion-menu-button` actions, choose who contr
|
|
|
100
102
|
const rail = await enableVerticalControlArea({ buttonProjection: 'source' });
|
|
101
103
|
```
|
|
102
104
|
|
|
103
|
-
Both `enableVerticalControlArea()` and `enableNativeUIShell()` accept the option. Use one runtime, and destroy it before restarting with different options. Either mode preserves actions, disabled state and grouping. These appearance settings do not affect horizontal controls, source elements or Web fallback clones, so compare the native appearance on supported iOS.
|
|
105
|
+
Both `enableVerticalControlArea()` and `enableNativeUIShell()` accept the option. Use one runtime, and destroy it before restarting with different options. Either mode preserves actions, disabled state and grouping. Disabled icons use the native disabled appearance. These appearance settings do not affect horizontal controls, source elements or Web fallback clones, so compare the native appearance on supported iOS.
|
|
104
106
|
|
|
105
107
|
**Migration from the experimental releases:** the default changes from source styling to `system`. Set `buttonProjection: 'source'` to retain the previous projection behavior.
|
|
106
108
|
|
|
@@ -64,21 +64,10 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
64
64
|
// Wire stays active+focused; local session drives chrome (idle / presented / focused).
|
|
65
65
|
private enum Session: Equatable { case idle, presented, focused }
|
|
66
66
|
|
|
67
|
-
private final class IdleBarDelegate: NSObject, UITabBarDelegate {
|
|
68
|
-
var select: ((String) -> Void)?
|
|
69
|
-
func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) {
|
|
70
|
-
guard item.isEnabled, let id = item.accessibilityIdentifier else { return }
|
|
71
|
-
select?(id)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
67
|
let surface = ShellSearchHost()
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
private
|
|
79
|
-
private let idleBarDelegate = IdleBarDelegate()
|
|
80
|
-
/// Resting search trigger pinned to the Web FAB while `available && !active`.
|
|
81
|
-
private var searchTrigger: UIButton?
|
|
68
|
+
private var restingBarFrame: CGRect?
|
|
69
|
+
private var restingBarAnchor: ShellTabBar.Anchor?
|
|
70
|
+
private var fittingTabBar = false
|
|
82
71
|
private let search = UISearchController(searchResultsController: nil)
|
|
83
72
|
private let inputDelegate = ShellSearchInputDelegate()
|
|
84
73
|
private var searchTab: UISearchTab!
|
|
@@ -88,8 +77,6 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
88
77
|
private var closing = false
|
|
89
78
|
private var editingSequence = 0
|
|
90
79
|
private var valueVersion = -1
|
|
91
|
-
private var lastLayout = ""
|
|
92
|
-
private var layoutItems: [ShellItemContent] = []
|
|
93
80
|
private var lockedWebFrame: CGRect? // frozen while search is active (width changes re-lock)
|
|
94
81
|
private var session: Session = .idle
|
|
95
82
|
private var focusWork: DispatchWorkItem?
|
|
@@ -109,14 +96,6 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
109
96
|
delegate = self
|
|
110
97
|
mode = .tabBar
|
|
111
98
|
ShellTabBar.configureLayout(tabBar)
|
|
112
|
-
ShellTabBar.configureLayout(idleBar)
|
|
113
|
-
// Must not use self as idleBar.delegate — UITabBarController asserts item↔VC pairing.
|
|
114
|
-
idleBar.delegate = idleBarDelegate
|
|
115
|
-
idleBarDelegate.select = { [weak self] id in
|
|
116
|
-
self?.armPendingSelection(id)
|
|
117
|
-
self?.activate?(id)
|
|
118
|
-
}
|
|
119
|
-
idleBar.isHidden = true
|
|
120
99
|
search.obscuresBackgroundDuringPresentation = false
|
|
121
100
|
search.hidesNavigationBarDuringPresentation = false
|
|
122
101
|
search.searchBar.delegate = self
|
|
@@ -206,10 +185,7 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
206
185
|
self.pendingExpiryWork = nil
|
|
207
186
|
guard let pending = self.pendingSelection, CFAbsoluteTimeGetCurrent() >= pending.until else { return }
|
|
208
187
|
self.pendingSelection = nil
|
|
209
|
-
//
|
|
210
|
-
if let item = self.idleBar.items?.first(where: { $0.accessibilityIdentifier == self.selectedID }) {
|
|
211
|
-
self.idleBar.selectedItem = item
|
|
212
|
-
}
|
|
188
|
+
// Revert to the last DOM selection if Ionic did not accept the action.
|
|
213
189
|
if let selected = self.ordinary[self.selectedID], self.selectedTab !== selected {
|
|
214
190
|
self.selectedTab = selected
|
|
215
191
|
}
|
|
@@ -244,7 +220,6 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
244
220
|
parent.addChild(self)
|
|
245
221
|
container.addSubview(surface)
|
|
246
222
|
surface.addSubview(view)
|
|
247
|
-
if idleBar.superview !== surface { surface.addSubview(idleBar) }
|
|
248
223
|
didMove(toParent: parent)
|
|
249
224
|
}
|
|
250
225
|
|
|
@@ -254,9 +229,7 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
254
229
|
clearPendingSelection()
|
|
255
230
|
applySession(.idle, selectingSearchTab: false)
|
|
256
231
|
willMove(toParent: nil)
|
|
257
|
-
|
|
258
|
-
searchTrigger?.removeFromSuperview()
|
|
259
|
-
searchTrigger = nil
|
|
232
|
+
restingBarFrame = nil
|
|
260
233
|
surface.removeFromSuperview()
|
|
261
234
|
view.removeFromSuperview()
|
|
262
235
|
removeFromParent()
|
|
@@ -270,18 +243,12 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
270
243
|
editingSequence = 0
|
|
271
244
|
valueVersion = -1
|
|
272
245
|
}
|
|
273
|
-
let previousAvailable = self.configuration?.available
|
|
274
246
|
let wasActive = self.configuration.map { $0.available && $0.active } ?? false
|
|
275
247
|
self.configuration = configuration
|
|
276
248
|
let available = configuration.available
|
|
277
249
|
let active = available && configuration.active
|
|
278
250
|
let wanted = wantedSession(active: active, focused: configuration.focused)
|
|
279
251
|
let items = snapshot.items
|
|
280
|
-
let content = items.map(\.content)
|
|
281
|
-
if layoutItems != content {
|
|
282
|
-
layoutItems = content
|
|
283
|
-
lastLayout = ""
|
|
284
|
-
}
|
|
285
252
|
let ids = items.map(\.id)
|
|
286
253
|
for id in Array(ordinary.keys) where !ids.contains(id) { ordinary.removeValue(forKey: id) }
|
|
287
254
|
for item in items {
|
|
@@ -304,10 +271,8 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
304
271
|
searchTab.image = rendering.image(trigger.content)
|
|
305
272
|
searchTab.isEnabled = !configuration.disabled
|
|
306
273
|
let requested = ids.compactMap { ordinary[$0] } + (available ? [searchTab!] : [])
|
|
307
|
-
if !active,
|
|
308
|
-
// Leave / availability flips rebuild tabs (UISearchTab morph cleanup when leaving).
|
|
274
|
+
if !active, tabs.isEmpty || tabs.map(\.identifier) != requested.map(\.identifier) {
|
|
309
275
|
tabs = requested
|
|
310
|
-
lastLayout = ""
|
|
311
276
|
}
|
|
312
277
|
for item in items {
|
|
313
278
|
if let nativeItem = ordinary[item.id]?.viewController?.tabBarItem {
|
|
@@ -316,6 +281,7 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
316
281
|
ShellTabBar.applyBadge(item.content.badge, to: nativeItem, rendering: rendering)
|
|
317
282
|
}
|
|
318
283
|
}
|
|
284
|
+
searchTab.viewController?.tabBarItem.accessibilityLabel = trigger.content.accessibilityLabel
|
|
319
285
|
let prominent = NSSelectorFromString("setProminentTabIdentifier:")
|
|
320
286
|
if responds(to: prominent) { setValue(available ? searchTab.identifier : nil, forKey: "prominentTabIdentifier") }
|
|
321
287
|
let field = configuration.field
|
|
@@ -334,7 +300,6 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
334
300
|
inputDelegate.original = search.searchBar.searchTextField.delegate
|
|
335
301
|
search.searchBar.searchTextField.delegate = inputDelegate
|
|
336
302
|
}
|
|
337
|
-
let layout = "\(webFrame):\(barFrame):\(triggerFrame):\(available):\(snapshot.rtl)"
|
|
338
303
|
// Resting keeps closing=true so hidden UISearchBar noise cannot emit; clear it while active.
|
|
339
304
|
closing = !active
|
|
340
305
|
surface.isHidden = false
|
|
@@ -344,17 +309,8 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
344
309
|
if tabs.isEmpty || tabs.map(\.identifier) != requested.map(\.identifier) {
|
|
345
310
|
tabs = requested
|
|
346
311
|
}
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
self.searchTrigger?.isHidden = true
|
|
350
|
-
self.view.isHidden = false
|
|
351
|
-
self.view.frame = self.surface.bounds
|
|
352
|
-
}
|
|
353
|
-
if !wasActive {
|
|
354
|
-
UIView.transition(with: surface, duration: 0.35, options: [.transitionCrossDissolve, .beginFromCurrentState], animations: reveal)
|
|
355
|
-
} else {
|
|
356
|
-
reveal()
|
|
357
|
-
}
|
|
312
|
+
restingBarFrame = nil
|
|
313
|
+
view.frame = surface.bounds
|
|
358
314
|
let nextLock = surface.bounds.isEmpty ? webFrame : surface.frame
|
|
359
315
|
if let locked = lockedWebFrame, abs(locked.width - webFrame.width) > 0.5 {
|
|
360
316
|
// Rotation / size-class change: adopt the new width while still ignoring keyboard shrink.
|
|
@@ -370,51 +326,68 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
370
326
|
|
|
371
327
|
surface.frame = webFrame
|
|
372
328
|
view.semanticContentAttribute = snapshot.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
373
|
-
|
|
329
|
+
tabBar.semanticContentAttribute = snapshot.rtl ? .forceRightToLeft : .forceLeftToRight
|
|
330
|
+
tabBar.accessibilityIdentifier = snapshot.id
|
|
374
331
|
if lockedWebFrame != nil {
|
|
375
332
|
lockedWebFrame = nil
|
|
376
|
-
lastLayout = ""
|
|
377
333
|
}
|
|
378
334
|
applySession(.idle, selectingSearchTab: false)
|
|
379
335
|
|
|
380
|
-
|
|
381
|
-
if idleBar.superview !== surface { surface.addSubview(idleBar) }
|
|
382
|
-
let localBar = surface.convert(barFrame, from: surface.superview)
|
|
383
|
-
let localTrigger = surface.convert(triggerFrame, from: surface.superview)
|
|
384
|
-
// Apply optimistic selection to the visible idle bar before reconciling the hidden controller.
|
|
385
|
-
ShellTabBar.update(idleBar, node: snapshot, rendering: rendering, pendingSelection: &pendingSelection)
|
|
336
|
+
view.frame = surface.bounds
|
|
386
337
|
resolveOrdinarySelection(items, fallback: requested.first)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
if searchTrigger !== triggerButton {
|
|
395
|
-
searchTrigger?.removeFromSuperview()
|
|
396
|
-
searchTrigger = triggerButton
|
|
397
|
-
surface.addSubview(triggerButton)
|
|
398
|
-
}
|
|
399
|
-
triggerButton.frame = localTrigger
|
|
400
|
-
}
|
|
338
|
+
restingBarFrame = surface.convert(barFrame, from: surface.superview)
|
|
339
|
+
restingBarAnchor = snapshot.tabBarAnchor
|
|
340
|
+
view.setNeedsLayout()
|
|
341
|
+
view.layoutIfNeeded()
|
|
342
|
+
return fitRestingTabBar()
|
|
343
|
+
}
|
|
401
344
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
345
|
+
override func viewDidLayoutSubviews() {
|
|
346
|
+
super.viewDidLayoutSubviews()
|
|
347
|
+
if session == .idle { _ = fitRestingTabBar() }
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/// Match the ordinary tab platter to the source ion-tab-bar by resizing
|
|
351
|
+
/// the controller's view, so UIKit can keep managing its tab bar through
|
|
352
|
+
/// search expansion and dismissal.
|
|
353
|
+
private func fitRestingTabBar() -> Bool {
|
|
354
|
+
guard let bounds = restingBarFrame, !fittingTabBar else { return true }
|
|
355
|
+
fittingTabBar = true
|
|
356
|
+
defer { fittingTabBar = false }
|
|
357
|
+
tabBar.layoutIfNeeded()
|
|
358
|
+
for _ in 0..<2 {
|
|
359
|
+
let regions = ShellTabBar.contentViews(tabBar)
|
|
360
|
+
func controlCount(_ view: UIView) -> Int {
|
|
361
|
+
(view is UIControl ? 1 : 0) + view.subviews.reduce(0) { $0 + controlCount($1) }
|
|
362
|
+
}
|
|
363
|
+
// Ordinary tabs contain their item controls; Search contains one.
|
|
364
|
+
// For a single tab, UIKit places ordinary tabs at the leading edge.
|
|
365
|
+
let rtl = tabBar.effectiveUserInterfaceLayoutDirection == .rightToLeft
|
|
366
|
+
guard let region = regions.max(by: {
|
|
367
|
+
let lhs = controlCount($0), rhs = controlCount($1)
|
|
368
|
+
if lhs != rhs { return lhs < rhs }
|
|
369
|
+
return rtl ? $0.frame.midX < $1.frame.midX : $0.frame.midX > $1.frame.midX
|
|
370
|
+
}) else { return false }
|
|
371
|
+
let content = region.frame
|
|
372
|
+
let contents = regions.map { $0.frame }
|
|
373
|
+
let measured = tabBar.convert(content, to: surface)
|
|
374
|
+
let width = bounds.width - measured.width
|
|
375
|
+
let x = bounds.minX + (bounds.width - measured.width) * (restingBarAnchor?.x ?? 0) - measured.minX
|
|
376
|
+
let y = bounds.minY + (bounds.height - measured.height) * (restingBarAnchor?.y ?? 0) - measured.minY
|
|
377
|
+
// UIKit rounds the platter frame; a subpoint difference must not
|
|
378
|
+
// keep invalidating its layout during search transitions.
|
|
379
|
+
if abs(width) <= 0.5 && abs(x) <= 0.5 && abs(y) <= 0.5 { break }
|
|
380
|
+
var frame = view.frame
|
|
381
|
+
// The controller also contains the separate search button. Include
|
|
382
|
+
// that space in its width limit without enlarging capped tab groups.
|
|
383
|
+
let searchWidth = contents.filter { $0 != content }.reduce(CGFloat(0)) { $0 + $1.width }
|
|
384
|
+
frame.size.width = min(frame.width + width, surface.bounds.width + searchWidth)
|
|
385
|
+
frame.origin.x += x
|
|
386
|
+
frame.origin.y += y
|
|
387
|
+
if abs(frame.width - view.frame.width) <= 0.5 && abs(x) <= 0.5 && abs(y) <= 0.5 { break }
|
|
388
|
+
view.frame = frame
|
|
389
|
+
view.setNeedsLayout()
|
|
390
|
+
view.layoutIfNeeded()
|
|
418
391
|
}
|
|
419
392
|
return true
|
|
420
393
|
}
|
|
@@ -422,6 +395,7 @@ final class ShellSearchController: UITabBarController, UITabBarControllerDelegat
|
|
|
422
395
|
func tabBarController(_ tabBarController: UITabBarController, shouldSelectTab tab: UITab) -> Bool {
|
|
423
396
|
guard let configuration else { return false }
|
|
424
397
|
if tab === searchTab {
|
|
398
|
+
restingBarFrame = nil
|
|
425
399
|
clearPendingSelection()
|
|
426
400
|
activate?(configuration.trigger.id)
|
|
427
401
|
return true
|
|
@@ -203,13 +203,19 @@ enum ShellTabBar {
|
|
|
203
203
|
}
|
|
204
204
|
|
|
205
205
|
private static func contentFrame(_ tabBar: UITabBar) -> CGRect? {
|
|
206
|
+
contentFrames(tabBar).reduce(nil) { (result: CGRect?, frame) in result.map { $0.union(frame) } ?? frame }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
static func contentFrames(_ tabBar: UITabBar) -> [CGRect] {
|
|
210
|
+
contentViews(tabBar).map { $0.frame }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static func contentViews(_ tabBar: UITabBar) -> [UIView] {
|
|
206
214
|
func containsControl(_ view: UIView) -> Bool {
|
|
207
215
|
view is UIControl || view.subviews.contains(where: containsControl)
|
|
208
216
|
}
|
|
209
217
|
return tabBar.subviews
|
|
210
218
|
.filter { !$0.isHidden && $0.alpha > 0 && containsControl($0) }
|
|
211
|
-
.
|
|
212
|
-
.filter { !$0.isEmpty }
|
|
213
|
-
.reduce(nil) { (result: CGRect?, frame) in result.map { $0.union(frame) } ?? frame }
|
|
219
|
+
.filter { !$0.frame.isEmpty }
|
|
214
220
|
}
|
|
215
221
|
}
|