@rdlabo/ionic-theme-ios27 1.2.0 → 1.2.1

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.
Files changed (69) hide show
  1. package/dist/css/components/ion-button.css +1 -1
  2. package/dist/css/components/ion-toolbar.css +1 -1
  3. package/dist/css/ionic-theme-ios27.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/native/components/index.js +2 -2
  6. package/dist/native/components/index.js.map +1 -1
  7. package/dist/native/prehide.d.ts.map +1 -1
  8. package/dist/native/prehide.js +6 -2
  9. package/dist/native/prehide.js.map +1 -1
  10. package/dist/native/runtime.d.ts.map +1 -1
  11. package/dist/native/runtime.js +4 -3
  12. package/dist/native/runtime.js.map +1 -1
  13. package/dist/native/shared/dom.d.ts +2 -0
  14. package/dist/native/shared/dom.d.ts.map +1 -1
  15. package/dist/native/shared/dom.js +5 -1
  16. package/dist/native/shared/dom.js.map +1 -1
  17. package/dist/native/vertical-bars-web.d.ts.map +1 -1
  18. package/dist/native/vertical-bars-web.js +4 -2
  19. package/dist/native/vertical-bars-web.js.map +1 -1
  20. package/dist/native-integration/index.d.ts +1 -1
  21. package/dist/native-integration/index.js.map +1 -1
  22. package/dist/native-integration/transition.d.ts +1 -1
  23. package/dist/native-integration/transition.js.map +1 -1
  24. package/dist/sheets-of-glass/animations.d.ts +1 -1
  25. package/dist/sheets-of-glass/animations.js +1 -1
  26. package/dist/sheets-of-glass/animations.js.map +1 -1
  27. package/dist/sheets-of-glass/index.js +1 -1
  28. package/dist/sheets-of-glass/index.js.map +1 -1
  29. package/dist/tab-bar-searchable/animations/enter.d.ts +1 -1
  30. package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -1
  31. package/dist/tab-bar-searchable/animations/enter.js +1 -1
  32. package/dist/tab-bar-searchable/animations/enter.js.map +1 -1
  33. package/dist/tab-bar-searchable/animations/leave.d.ts +1 -1
  34. package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -1
  35. package/dist/tab-bar-searchable/animations/leave.js +1 -1
  36. package/dist/tab-bar-searchable/animations/leave.js.map +1 -1
  37. package/dist/tab-bar-searchable/index.js +1 -1
  38. package/dist/tab-bar-searchable/index.js.map +1 -1
  39. package/dist/transition/index.d.ts +1 -1
  40. package/dist/transition/index.js +1 -1
  41. package/dist/transition/index.js.map +1 -1
  42. package/dist/transition/ios.transition.d.ts +1 -1
  43. package/dist/transition/ios.transition.js.map +1 -1
  44. package/dist/utils.d.ts +1 -1
  45. package/dist/utils.js.map +1 -1
  46. package/docs/native-ui-shell.md +2 -2
  47. package/docs/vertical-bars.md +3 -1
  48. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +65 -91
  49. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellTabBar.swift +9 -3
  50. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +4 -2
  51. package/package.json +1 -1
  52. package/src/native/components/index.ts +3 -2
  53. package/src/native/prehide.ts +6 -1
  54. package/src/native/runtime.ts +5 -1
  55. package/src/native/shared/dom.ts +14 -0
  56. package/src/native/vertical-bars-web.ts +11 -1
  57. package/src/native-integration/index.ts +1 -1
  58. package/src/native-integration/transition.ts +1 -1
  59. package/src/sheets-of-glass/animations.ts +2 -2
  60. package/src/sheets-of-glass/index.ts +2 -2
  61. package/src/styles/components/ion-button.scss +47 -0
  62. package/src/styles/components/ion-toolbar.scss +62 -7
  63. package/src/styles/vertical-bars.scss +23 -0
  64. package/src/tab-bar-searchable/animations/enter.ts +2 -1
  65. package/src/tab-bar-searchable/animations/leave.ts +2 -1
  66. package/src/tab-bar-searchable/index.ts +1 -1
  67. package/src/transition/index.ts +3 -3
  68. package/src/transition/ios.transition.ts +1 -1
  69. package/src/utils.ts +1 -1
@@ -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"}
@@ -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`), instead of demoting back to a separate ordinary control identity. Resting chrome (including Album before search opens) uses the same `UITabBar` + `ShellTabBar.fit` path as ordinary tabs, with the search trigger pinned to the FAB when available; `UISearchTab` is shown only while the search session is active. Flipping availability or entering/leaving search crossfades between those layers. Register before the destination page finishes entering (for example in `ionViewWillEnter`) so the first visit does not paint ordinary tabs and then swap.
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
- With horizontal tabs, 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 (no Ionic `fit` mid-session). Leave re-fits ordinary tabs to `ion-tab-bar` and search to the FAB. 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 and the search icon are projected from Ionic, including `ion-icon name`.
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
 
@@ -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
- /// Ordinary `UITabBar` chrome while searchable is registered but the session is idle.
77
- /// Matches `ShellTabBar.fit` so Index/Album resting tabs do not jump.
78
- private let idleBar = UITabBar()
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
- // No later apply: revert idle chrome + controller selection to the last DOM selected id.
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
- idleBar.removeFromSuperview()
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, wasActive || tabs.isEmpty || tabs.map(\.identifier) != requested.map(\.identifier) {
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
- let reveal = {
348
- self.idleBar.isHidden = true
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
- idleBar.semanticContentAttribute = snapshot.rtl ? .forceRightToLeft : .forceLeftToRight
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
- // Resting chrome: ordinary UITabBar (+ FAB trigger when available). UISearchTab stays off-screen.
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
- if !available {
388
- searchTrigger?.removeFromSuperview()
389
- searchTrigger = nil
390
- } else {
391
- let triggerButton = ShellButton.render(configuration.trigger.content, glass: true, existing: searchTrigger, rendering: rendering) { [weak self] id in
392
- self?.activate?(id)
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
- let revealResting = {
403
- self.view.isHidden = true
404
- self.idleBar.isHidden = false
405
- self.idleBar.overrideUserInterfaceStyle = snapshot.dark ? .dark : .light
406
- self.searchTrigger?.isHidden = !available
407
- self.searchTrigger?.overrideUserInterfaceStyle = snapshot.dark ? .dark : .light
408
- }
409
- let animateChrome = wasActive || (previousAvailable != nil && previousAvailable != available)
410
- if animateChrome {
411
- UIView.transition(with: surface, duration: 0.35, options: [.transitionCrossDissolve, .beginFromCurrentState], animations: revealResting)
412
- } else {
413
- revealResting()
414
- }
415
- if lastLayout != layout {
416
- guard ShellTabBar.fit(idleBar, node: snapshot, bounds: localBar) else { return false }
417
- lastLayout = layout
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
- .map { $0.frame }
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
  }
@@ -45,9 +45,11 @@ final class ShellVerticalBarsModel: ObservableObject {
45
45
  func apply(_ controls: [ShellControl], rendering: ShellRendering,
46
46
  now: CFAbsoluteTime = CFAbsoluteTimeGetCurrent()) {
47
47
  func item(_ source: ShellItem) -> Item {
48
- Item(id: source.id, label: source.content.label,
48
+ let image = rendering.image(source.content)
49
+ return Item(id: source.id, label: source.content.label,
49
50
  accessibilityLabel: source.content.accessibilityLabel,
50
- image: rendering.image(source.content),
51
+ // UIKit toolbar extraction uses the UIImage's rendering mode.
52
+ image: source.content.disabled ? image?.withRenderingMode(.alwaysTemplate) : image,
51
53
  clear: source.content.buttonFill == .clear,
52
54
  background: source.content.backgroundColor.map { Color(uiColor: rendering.color($0)) },
53
55
  borderColor: source.content.borderColor.map { Color(uiColor: rendering.color($0)) },
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@rdlabo/ionic-theme-ios27",
3
3
  "private": false,
4
- "version": "1.2.0",
4
+ "version": "1.2.1",
5
5
  "description": "iOS27 Theme for Ionic Framework",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -6,7 +6,7 @@ import * as menuButton from './ion-menu-button';
6
6
  import * as tabBar from './ion-tab-bar';
7
7
  import * as segment from './ion-segment';
8
8
  import * as fab from './ion-fab';
9
- import { isDisabledButtonGroupChild, isVerticalBarsSource, visible } from '../shared/dom';
9
+ import { isDisabledButtonGroupChild, isVerticalBarsSource, visible, withoutPrehide } from '../shared/dom';
10
10
  import type { Candidate, Identify } from '../shared/candidate';
11
11
 
12
12
  // Static composition only. Each component declares its own tag, discovery and reader.
@@ -48,5 +48,6 @@ export const readCandidate = (element: HTMLElement, id: Identify, options: Verti
48
48
  )
49
49
  return;
50
50
  if (element.contains(element.ownerDocument.activeElement)) return;
51
- return components.find((component) => component.tag === element.localName)?.read(element, id, options);
51
+ // Read icon and control geometry before restoring prehide, which can collapse the toolbar.
52
+ return withoutPrehide(element, () => components.find((component) => component.tag === element.localName)?.read(element, id, options));
52
53
  };
@@ -1,5 +1,5 @@
1
1
  import { modalUsesVerticalBars } from './shared/modal';
2
- import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core';
2
+ import { LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE, LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE } from '@ionic/core/components/index.js';
3
3
  import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
4
4
  import {
5
5
  childElements,
@@ -16,6 +16,7 @@ import {
16
16
  prehideRootClass,
17
17
  rejectedClass,
18
18
  setVerticalBarsPlacement,
19
+ syncToolbarText,
19
20
  } from './shared/dom';
20
21
 
21
22
  const overlays = 'ion-menu, ion-popover';
@@ -133,6 +134,7 @@ export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: ()
133
134
  scopes.set(scope, owned);
134
135
  };
135
136
  const reconcile = () => {
137
+ syncToolbarText(doc);
136
138
  const verticalBars = root();
137
139
  if (!verticalBars) {
138
140
  Array.from(scopes.keys()).forEach(release);
@@ -200,6 +202,8 @@ export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: ()
200
202
  doc.defaultView?.addEventListener('nativeUIShellRefresh', reconcile, { signal: listeners.signal });
201
203
  reconcile();
202
204
  const mutationRelevant = (record: MutationRecord) => {
205
+ if (record.type === 'characterData') return record.target.parentElement?.matches('ion-toolbar') === true;
206
+ if (record.type === 'childList' && asElement(record.target)?.matches('ion-toolbar')) return true;
203
207
  if (record.type === 'childList')
204
208
  return [...Array.from(record.addedNodes), ...Array.from(record.removedNodes)].some(
205
209
  (node) =>
@@ -228,6 +232,7 @@ export const prehideVerticalBarsToolbarSources = (doc: Document): { suspend: ()
228
232
  observer.observe(doc.documentElement, {
229
233
  subtree: true,
230
234
  childList: true,
235
+ characterData: true,
231
236
  attributes: true,
232
237
  attributeOldValue: true,
233
238
  attributeFilter: ['class', 'data-shell', 'hidden', 'inert', 'icon', 'color'],
@@ -1,6 +1,6 @@
1
1
  import { inVerticalBarsSurface, topModal, modalUsesVerticalBars, modalVerticalBarFrame } from './shared/modal';
2
2
  import type { PluginListenerHandle } from '@capacitor/core';
3
- import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core';
3
+ import { LIFECYCLE_WILL_ENTER, LIFECYCLE_WILL_LEAVE, LIFECYCLE_DID_ENTER, LIFECYCLE_DID_LEAVE } from '@ionic/core/components/index.js';
4
4
  import { VERTICAL_BARS_TRANSITION_CANCELED, getNativeSearchBindings, setNativeUIShellIntegration } from '../native-integration';
5
5
  import { createSearchSupport } from './components/searchable-tabs';
6
6
  import type {
@@ -18,6 +18,8 @@ import {
18
18
  isVerticalBarsSource,
19
19
  preferredVerticalBarsBack,
20
20
  marker,
21
+ syncToolbarText,
22
+ toolbarTextMarker,
21
23
  prehideOnlyMutation,
22
24
  rejectedClass,
23
25
  unprojected,
@@ -299,6 +301,7 @@ export const createRuntime = async (
299
301
  frame = 0;
300
302
  dirty = false;
301
303
  pending = true;
304
+ syncToolbarText(doc);
302
305
  handoffInstant = tabSwitchHandoff || handoffAcrossPending || modalPresentHandoff || win.performance.now() < handoffUntil;
303
306
  modalPresentHandoff = false;
304
307
  try {
@@ -466,6 +469,7 @@ export const createRuntime = async (
466
469
  records.some(
467
470
  (record) =>
468
471
  record.attributeName !== marker &&
472
+ record.attributeName !== toolbarTextMarker &&
469
473
  record.attributeName !== fadeMarker &&
470
474
  !prehideOnlyMutation(record) &&
471
475
  !(record.attributeName === 'style' && measuringPointerPages.has(record.target as HTMLElement)) &&
@@ -193,6 +193,20 @@ export const isVerticalBarsToolbarGroup = (element: HTMLElement): boolean => {
193
193
  );
194
194
  };
195
195
 
196
+ export const toolbarTextMarker = 'data-native-ui-shell-toolbar-text';
197
+
198
+ export const syncToolbarText = (doc: Document): void => {
199
+ for (const toolbar of Array.from(
200
+ doc.querySelectorAll<HTMLElement>(`ion-app.ios-theme-vertical-bars ion-toolbar, ion-toolbar[${toolbarTextMarker}]`),
201
+ )) {
202
+ toolbar.toggleAttribute(
203
+ toolbarTextMarker,
204
+ !!toolbar.closest('ion-app.ios-theme-vertical-bars') &&
205
+ childNodesOf(toolbar).some((node) => node.nodeType === Node.TEXT_NODE && !!node.textContent?.trim()),
206
+ );
207
+ }
208
+ };
209
+
196
210
  export const activateProjectedElement = (element: HTMLElement): void => {
197
211
  const target = element.matches('ion-button, ion-back-button, ion-menu-button')
198
212
  ? element.shadowRoot?.querySelector<HTMLElement>('[part~="native"]')