@rdlabo/ionic-theme-ios27 1.2.0-beta.pr246.sha237333ceac21 → 1.2.0

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 (32) hide show
  1. package/dist/css/components/ion-menu.css +1 -1
  2. package/dist/css/ionic-theme-ios27.css +1 -1
  3. package/dist/css/utils/searchable.css +1 -1
  4. package/dist/css/vertical-bars.css +1 -1
  5. package/dist/native/components/searchable-tabs.js +1 -1
  6. package/dist/native/components/searchable-tabs.js.map +1 -1
  7. package/dist/native/vertical-bars-web-search.d.ts +9 -0
  8. package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
  9. package/dist/native/vertical-bars-web-search.js +84 -0
  10. package/dist/native/vertical-bars-web-search.js.map +1 -0
  11. package/dist/native/vertical-bars-web.d.ts.map +1 -1
  12. package/dist/native/vertical-bars-web.js +20 -16
  13. package/dist/native/vertical-bars-web.js.map +1 -1
  14. package/dist/tab-bar-searchable/dismiss-button.d.ts +4 -0
  15. package/dist/tab-bar-searchable/dismiss-button.d.ts.map +1 -0
  16. package/dist/tab-bar-searchable/dismiss-button.js +34 -0
  17. package/dist/tab-bar-searchable/dismiss-button.js.map +1 -0
  18. package/dist/tab-bar-searchable/index.d.ts.map +1 -1
  19. package/dist/tab-bar-searchable/index.js +6 -0
  20. package/dist/tab-bar-searchable/index.js.map +1 -1
  21. package/docs/native-ui-shell.md +1 -1
  22. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +13 -0
  23. package/package.json +1 -1
  24. package/src/native/components/searchable-tabs.ts +2 -2
  25. package/src/native/vertical-bars-web-search.ts +80 -0
  26. package/src/native/vertical-bars-web.ts +30 -12
  27. package/src/styles/components/ion-menu.scss +2 -2
  28. package/src/styles/ionic-theme-ios27.scss +2 -0
  29. package/src/styles/utils/searchable.scss +80 -0
  30. package/src/styles/vertical-bars.scss +32 -0
  31. package/src/tab-bar-searchable/dismiss-button.ts +33 -0
  32. package/src/tab-bar-searchable/index.ts +6 -0
@@ -1 +1 @@
1
- {"version":3,"file":"vertical-bars-web.js","names":[],"sources":["../../src/native/vertical-bars-web.ts"],"sourcesContent":["import { inVerticalBarsSurface, topModal } from './shared/modal';\nimport type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellStatus } from './definitions';\nimport { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';\nimport {\n activateProjectedElement,\n createVerticalBarsPageState,\n verticalBarsActionCandidate,\n verticalBarsBackCandidate,\n verticalBarsEnteringPage,\n verticalBarsToolbarActions,\n isExcluded,\n isVerticalBarsToolbarGroup,\n preferredVerticalBarsBack,\n verticalBarsOwned,\n marker,\n prehideOnlyMutation,\n prehiddenClass,\n unprojected,\n withoutPrehide,\n} from './shared/dom';\n\nconst backProjectionClass = 'ios-theme-vertical-bars-back-button-projection';\nconst toolbarProjectionClass = 'ios-theme-vertical-bars-toolbar-projection';\nconst readyClass = 'ios-theme-vertical-bars-toolbar-ready';\nconst toolbarControlSize = 46;\nconst toolbarControlGap = 10;\n\ninterface ToolbarProjection {\n source: HTMLIonButtonsElement;\n projection: HTMLElement;\n actions: { source: HTMLElement; projection: HTMLElement; previousAriaHidden: string | null }[];\n}\n\ninterface ToolbarSource {\n group: HTMLIonButtonsElement;\n actions: HTMLElement[];\n}\n\nexport const createVerticalBarsWebProjection = (\n doc: Document,\n options: NativeUIShellOptions,\n enabled: () => boolean = () => true,\n): NativeUIShellHandle => {\n const win = doc.defaultView!;\n if (options.controls !== undefined && options.controls.toolbar !== true)\n return {\n getStatus: () => ({ state: 'web', projected: 0, updates: 0 }),\n suspend: async () => ({ resume: async () => {} }),\n destroy: async () => {},\n };\n let root: HTMLElement | undefined;\n let backSource: HTMLIonBackButtonElement | undefined;\n let backProjection: HTMLIonBackButtonElement | undefined;\n let previousBackAriaHidden: string | null = null;\n let toolbarProjections: ToolbarProjection[] = [];\n let suspended = 0;\n let stopped = false;\n let frame = 0;\n let updates = 0;\n let observingVerticalBars = false;\n const verticalBarsPages = createVerticalBarsPageState();\n let sourceObserver: MutationObserver | undefined;\n let waiters: (() => void)[] = [];\n const listeners = new AbortController();\n const verticalBarsRoot = () => doc.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');\n const projectedSources = () =>\n [backSource, ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source))].filter(\n (source): source is HTMLElement => !!source,\n );\n const isRendered = (element: HTMLElement) =>\n withoutPrehide(element, () => {\n const style = getComputedStyle(element);\n const rect = element.getBoundingClientRect();\n return element.isConnected && style.display !== 'none' && style.visibility === 'visible' && rect.width > 0 && rect.height > 0;\n });\n const inEligibleToolbar = (element: HTMLElement) =>\n !!verticalBarsRoot()?.contains(element) &&\n inVerticalBarsSurface(element) &&\n verticalBarsActionCandidate(element) &&\n !isExcluded(element, verticalBarsEnteringPage(element)) &&\n !verticalBarsPages.isDeparted(element);\n const isEligibleBack = (element: HTMLIonBackButtonElement) =>\n !!verticalBarsRoot()?.contains(element) &&\n inVerticalBarsSurface(element) &&\n verticalBarsOwned(element) &&\n verticalBarsBackCandidate(element) &&\n !isExcluded(element, verticalBarsEnteringPage(element)) &&\n !verticalBarsPages.isDeparted(element) &&\n unprojected(projectedSources(), () => isRendered(element));\n const pageOrder = (element: Element) => Array.from(doc.querySelectorAll('.ion-page')).indexOf(element.closest('.ion-page')!);\n const findBack = () => {\n const candidates = Array.from(doc.querySelectorAll<HTMLIonBackButtonElement>(`ion-back-button:not(.${backProjectionClass})`)).filter(\n isEligibleBack,\n );\n return preferredVerticalBarsBack(candidates, doc);\n };\n const findToolbarGroups = () => {\n const candidates = Array.from(doc.querySelectorAll<HTMLIonButtonsElement>(`ion-buttons:not(.${toolbarProjectionClass})`)).flatMap(\n (group): ToolbarSource[] => {\n const actions = verticalBarsToolbarActions(group).filter((action) => unprojected(projectedSources(), () => isRendered(action)));\n if (!actions.length) return [];\n if (!isVerticalBarsToolbarGroup(group)) return actions.filter(inEligibleToolbar).map((action) => ({ group, actions: [action] }));\n return inEligibleToolbar(group) && unprojected(projectedSources(), () => isRendered(group)) ? [{ group, actions }] : [];\n },\n );\n const highestPage = Math.max(...candidates.map(({ group }) => pageOrder(group)));\n return candidates.filter(({ group }) => pageOrder(group) === highestPage);\n };\n const isCurrentToolbarAction = (source: HTMLElement) => findToolbarGroups().some(({ actions }) => actions.includes(source));\n const restore = () => {\n backProjection?.remove();\n backProjection = undefined;\n if (backSource) {\n backSource.removeAttribute(marker);\n if (previousBackAriaHidden === null) backSource.removeAttribute('aria-hidden');\n else backSource.setAttribute('aria-hidden', previousBackAriaHidden);\n backSource.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n }\n backSource = undefined;\n previousBackAriaHidden = null;\n for (const { projection, actions } of toolbarProjections) {\n projection.remove();\n for (const { source, previousAriaHidden } of actions) {\n source.removeAttribute(marker);\n if (previousAriaHidden === null) source.removeAttribute('aria-hidden');\n else source.setAttribute('aria-hidden', previousAriaHidden);\n source.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n }\n }\n toolbarProjections = [];\n sourceObserver?.disconnect();\n sourceObserver = undefined;\n root?.classList.remove(readyClass);\n root = undefined;\n };\n const copyAttributes = (target: HTMLElement, original: HTMLElement) => {\n for (const name of ['id', 'slot', marker, 'aria-hidden']) target.removeAttribute(name);\n for (const attribute of Array.from(target.attributes))\n if (!['class', marker].includes(attribute.name) && !original.hasAttribute(attribute.name)) target.removeAttribute(attribute.name);\n for (const attribute of Array.from(original.attributes))\n if (!['id', 'slot', marker, 'aria-hidden'].includes(attribute.name)) target.setAttribute(attribute.name, attribute.value);\n target.classList.remove(prehiddenClass);\n };\n const syncBack = (target: HTMLIonBackButtonElement, original: HTMLIonBackButtonElement) => {\n copyAttributes(target, original);\n target.classList.add(backProjectionClass, 'ion-cloned-element');\n target.disabled = original.disabled;\n target.defaultHref = original.defaultHref;\n target.icon = original.icon;\n target.color = original.color;\n target.mode = original.mode;\n target.text = '';\n };\n const syncAction = (target: HTMLElement, original: HTMLElement) => {\n copyAttributes(target, original);\n target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');\n const label =\n original.getAttribute('aria-label') ??\n original.shadowRoot?.querySelector('[part~=native]')?.getAttribute('aria-label') ??\n original.textContent?.trim();\n if (label) target.setAttribute('aria-label', label);\n if ('disabled' in original) (target as HTMLIonButtonElement).disabled = (original as HTMLIonButtonElement).disabled;\n if (original.matches('ion-button')) {\n const icon = original.querySelector<HTMLElement>('ion-icon, svg');\n const clonedIcon = icon?.cloneNode(true) as HTMLElement | undefined;\n clonedIcon?.setAttribute('slot', 'icon-only');\n target.replaceChildren(...(clonedIcon ? [clonedIcon] : []));\n }\n };\n const sameSources = (back: HTMLIonBackButtonElement | undefined, groups: ToolbarSource[]) =>\n back === backSource &&\n groups.length === toolbarProjections.length &&\n groups.every(({ group, actions }, index) => {\n const current = toolbarProjections[index];\n return (\n group === current.source &&\n actions.length === current.actions.length &&\n actions.every((action, actionIndex) => action === current.actions[actionIndex].source)\n );\n });\n const syncExisting = () => {\n if (backSource && backProjection) syncBack(backProjection, backSource);\n let topOffset = backSource ? toolbarControlSize + toolbarControlGap : 0;\n for (const { projection, source, actions } of toolbarProjections) {\n if (actions.length === 1 && !projection.matches('ion-buttons')) syncAction(projection, actions[0].source);\n else copyAttributes(projection, source);\n projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');\n projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);\n if (projection.matches('ion-buttons')) actions.forEach(({ source: action, projection: clone }) => syncAction(clone, action));\n topOffset += actions.length * toolbarControlSize + toolbarControlGap;\n }\n };\n const project = (nextBack: HTMLIonBackButtonElement | undefined, groups: ToolbarSource[]) => {\n root = topModal(doc) ?? verticalBarsRoot()!;\n let topOffset = 0;\n if (nextBack) {\n backSource = nextBack;\n previousBackAriaHidden = nextBack.getAttribute('aria-hidden');\n backProjection = nextBack.cloneNode(false) as HTMLIonBackButtonElement;\n syncBack(backProjection, nextBack);\n backProjection.addEventListener(\n 'click',\n (event) => {\n event.preventDefault();\n event.stopImmediatePropagation();\n if (backSource && backSource === findBack()) activateProjectedElement(backSource);\n },\n { capture: true },\n );\n root.append(backProjection);\n nextBack.setAttribute(marker, '');\n nextBack.setAttribute('aria-hidden', 'true');\n nextBack.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n topOffset = toolbarControlSize + toolbarControlGap;\n }\n for (const { group, actions: sources } of groups) {\n const projection = (sources.length === 1 ? sources[0] : group).cloneNode(false) as HTMLElement;\n if (sources.length === 1) syncAction(projection, sources[0]);\n else copyAttributes(projection, group);\n projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');\n projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);\n const actions = sources.map((source) => {\n const clone = sources.length === 1 ? projection : (source.cloneNode(false) as HTMLElement);\n if (sources.length > 1) syncAction(clone, source);\n clone.addEventListener(\n 'click',\n (event) => {\n event.preventDefault();\n event.stopImmediatePropagation();\n if (isCurrentToolbarAction(source)) activateProjectedElement(source);\n },\n { capture: true },\n );\n if (sources.length > 1) projection.append(clone);\n const previousAriaHidden = source.getAttribute('aria-hidden');\n source.setAttribute(marker, '');\n source.setAttribute('aria-hidden', 'true');\n source.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n return { source, projection: clone, previousAriaHidden };\n });\n root.append(projection);\n toolbarProjections.push({ source: group, projection, actions });\n topOffset += actions.length * toolbarControlSize + toolbarControlGap;\n }\n if (nextBack || toolbarProjections.length) root.classList.add(readyClass);\n sourceObserver = new MutationObserver((records) => {\n if (\n records.some(\n (record) =>\n record.type !== 'attributes' || (![marker, 'aria-hidden'].includes(record.attributeName ?? '') && !prehideOnlyMutation(record)),\n )\n )\n schedule();\n });\n if (nextBack?.shadowRoot)\n sourceObserver.observe(nextBack.shadowRoot, { subtree: true, childList: true, attributes: true, attributeOldValue: true });\n for (const { group } of groups)\n sourceObserver.observe(group, { subtree: true, childList: true, attributes: true, attributeOldValue: true });\n };\n const performUpdate = () => {\n frame = 0;\n const currentRoot = verticalBarsRoot();\n if (observingVerticalBars !== !!currentRoot) {\n observingVerticalBars = !!currentRoot;\n observer.disconnect();\n observer.observe(doc.documentElement, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeOldValue: true,\n attributeFilter: observingVerticalBars ? undefined : ['class'],\n });\n }\n if (stopped || suspended || !currentRoot || !enabled()) return restore();\n const nextBack = findBack();\n const groups = findToolbarGroups();\n if (!nextBack && !groups.length) return restore();\n if (root === (topModal(doc) ?? currentRoot) && sameSources(nextBack, groups)) return syncExisting();\n restore();\n project(nextBack, groups);\n updates++;\n };\n const update = () => {\n try {\n performUpdate();\n } catch {\n restore();\n } finally {\n const pending = waiters;\n waiters = [];\n pending.forEach((resolve) => resolve());\n }\n };\n const schedule = (): Promise<void> => {\n if (stopped) return Promise.resolve();\n // Release Web ownership before the native runtime can acknowledge its next frame.\n if (!enabled()) restore();\n const done = new Promise<void>((resolve) => waiters.push(resolve));\n if (!frame) frame = win.requestAnimationFrame(update);\n return done;\n };\n const observer = new MutationObserver((records) => {\n const insideProjection = (target: Node) => {\n const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;\n const rootNode = target.getRootNode();\n const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;\n return !![element, shadowHost].some((candidate) => candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}`));\n };\n const verticalBarsChanged = records.some(\n (record) =>\n (record.type === 'attributes' && (record.target as Element).matches('ion-app')) ||\n (record.type === 'childList' &&\n Array.from(record.addedNodes).some(\n (node) =>\n node instanceof Element &&\n (node.matches('ion-app.ios-theme-vertical-bars') || !!node.querySelector('ion-app.ios-theme-vertical-bars')),\n )),\n );\n if (\n (observingVerticalBars &&\n records.some((record) => record.attributeName !== marker && !prehideOnlyMutation(record) && !insideProjection(record.target))) ||\n (!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record)))\n )\n schedule();\n });\n observer.observe(doc.documentElement, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeOldValue: true,\n attributeFilter: ['class'],\n });\n const pageLifecycle = (event: Event) => {\n const page = event.target;\n if (!(page instanceof HTMLElement) || !page.matches('.ion-page')) return;\n if (event.type === VERTICAL_BARS_TRANSITION_CANCELED) {\n const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;\n verticalBarsPages.cancel(entering, page);\n } else verticalBarsPages.lifecycle(event);\n if (verticalBarsRoot()) schedule();\n };\n for (const name of ['ionViewWillEnter', 'ionViewWillLeave', 'ionViewDidEnter', 'ionViewDidLeave'])\n doc.addEventListener(name, pageLifecycle, { capture: true, signal: listeners.signal });\n doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });\n for (const name of ['ionModalWillPresent', 'ionModalDidDismiss'])\n doc.addEventListener(name, schedule, { capture: true, signal: listeners.signal });\n win.addEventListener('resize', schedule, { signal: listeners.signal });\n win.addEventListener('nativeUIShellRefresh', schedule, { signal: listeners.signal });\n schedule();\n\n return {\n getStatus: (): NativeUIShellStatus => ({\n state: stopped ? 'stopped' : 'web',\n projected: Number(!!backSource) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),\n updates,\n }),\n async suspend() {\n suspended++;\n restore();\n let resumed = false;\n return {\n async resume() {\n if (resumed) return;\n resumed = true;\n suspended = Math.max(0, suspended - 1);\n if (!suspended) await schedule();\n },\n };\n },\n async destroy() {\n if (stopped) return;\n stopped = true;\n win.cancelAnimationFrame(frame);\n frame = 0;\n waiters.forEach((resolve) => resolve());\n waiters = [];\n observer.disconnect();\n sourceObserver?.disconnect();\n listeners.abort();\n restore();\n },\n };\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"vertical-bars-web.js","names":[],"sources":["../../src/native/vertical-bars-web.ts"],"sourcesContent":["import { createVerticalBarsWebSearch, searchProjectionClass } from './vertical-bars-web-search';\nimport { inVerticalBarsSurface, topModal } from './shared/modal';\nimport type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellStatus } from './definitions';\nimport { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';\nimport {\n activateProjectedElement,\n createVerticalBarsPageState,\n verticalBarsActionCandidate,\n verticalBarsBackCandidate,\n verticalBarsEnteringPage,\n verticalBarsToolbarActions,\n isExcluded,\n isShellDisabled,\n isVerticalBarsToolbarGroup,\n preferredVerticalBarsBack,\n verticalBarsOwned,\n marker,\n prehideOnlyMutation,\n prehiddenClass,\n unprojected,\n withoutPrehide,\n} from './shared/dom';\n\nconst backProjectionClass = 'ios-theme-vertical-bars-back-button-projection';\nconst toolbarProjectionClass = 'ios-theme-vertical-bars-toolbar-projection';\nconst readyClass = 'ios-theme-vertical-bars-toolbar-ready';\nconst toolbarControlSize = 46;\nconst toolbarControlGap = 10;\n\ninterface ToolbarProjection {\n source: HTMLIonButtonsElement;\n projection: HTMLElement;\n actions: { source: HTMLElement; projection: HTMLElement; previousAriaHidden: string | null }[];\n}\n\ninterface ToolbarSource {\n group: HTMLIonButtonsElement;\n actions: HTMLElement[];\n}\n\nexport const createVerticalBarsWebProjection = (\n doc: Document,\n options: NativeUIShellOptions,\n enabled: () => boolean = () => true,\n): NativeUIShellHandle => {\n const win = doc.defaultView!;\n const toolbarEnabled = options.controls === undefined || options.controls.toolbar === true;\n let root: HTMLElement | undefined;\n let backSource: HTMLIonBackButtonElement | undefined;\n let backProjection: HTMLIonBackButtonElement | undefined;\n let previousBackAriaHidden: string | null = null;\n let toolbarProjections: ToolbarProjection[] = [];\n let suspended = 0;\n let stopped = false;\n let frame = 0;\n let updates = 0;\n let observingVerticalBars = false;\n const verticalBarsPages = createVerticalBarsPageState();\n let sourceObserver: MutationObserver | undefined;\n let waiters: (() => void)[] = [];\n const listeners = new AbortController();\n const verticalBarsRoot = () => doc.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');\n const projectedSources = () =>\n [backSource, search.source, ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source))].filter(\n (source): source is HTMLElement => !!source,\n );\n const isRendered = (element: HTMLElement) =>\n withoutPrehide(element, () => {\n const style = getComputedStyle(element);\n const rect = element.getBoundingClientRect();\n return element.isConnected && style.display !== 'none' && style.visibility === 'visible' && rect.width > 0 && rect.height > 0;\n });\n const search = createVerticalBarsWebSearch(\n doc,\n (element) =>\n (options.controls === undefined || options.controls.tabs === true) &&\n !!verticalBarsRoot()?.contains(element) &&\n inVerticalBarsSurface(element) &&\n !isExcluded(element, verticalBarsEnteringPage(element)) &&\n !isShellDisabled(element) &&\n !verticalBarsPages.isDeparted(element) &&\n isRendered(element),\n () => {\n void schedule();\n },\n );\n const inEligibleToolbar = (element: HTMLElement) =>\n !!verticalBarsRoot()?.contains(element) &&\n inVerticalBarsSurface(element) &&\n verticalBarsActionCandidate(element) &&\n !isExcluded(element, verticalBarsEnteringPage(element)) &&\n !verticalBarsPages.isDeparted(element);\n const isEligibleBack = (element: HTMLIonBackButtonElement) =>\n !!verticalBarsRoot()?.contains(element) &&\n inVerticalBarsSurface(element) &&\n verticalBarsOwned(element) &&\n verticalBarsBackCandidate(element) &&\n !isExcluded(element, verticalBarsEnteringPage(element)) &&\n !verticalBarsPages.isDeparted(element) &&\n unprojected(projectedSources(), () => isRendered(element));\n const pageOrder = (element: Element) => Array.from(doc.querySelectorAll('.ion-page')).indexOf(element.closest('.ion-page')!);\n const findBack = () => {\n const candidates = Array.from(doc.querySelectorAll<HTMLIonBackButtonElement>(`ion-back-button:not(.${backProjectionClass})`)).filter(\n isEligibleBack,\n );\n return preferredVerticalBarsBack(candidates, doc);\n };\n const findToolbarGroups = () => {\n const candidates = Array.from(doc.querySelectorAll<HTMLIonButtonsElement>(`ion-buttons:not(.${toolbarProjectionClass})`)).flatMap(\n (group): ToolbarSource[] => {\n const actions = verticalBarsToolbarActions(group).filter((action) => unprojected(projectedSources(), () => isRendered(action)));\n if (!actions.length) return [];\n if (!isVerticalBarsToolbarGroup(group)) return actions.filter(inEligibleToolbar).map((action) => ({ group, actions: [action] }));\n return inEligibleToolbar(group) && unprojected(projectedSources(), () => isRendered(group)) ? [{ group, actions }] : [];\n },\n );\n const highestPage = Math.max(...candidates.map(({ group }) => pageOrder(group)));\n return candidates.filter(({ group }) => pageOrder(group) === highestPage);\n };\n const isCurrentToolbarAction = (source: HTMLElement) => findToolbarGroups().some(({ actions }) => actions.includes(source));\n const restore = () => {\n search.restore();\n backProjection?.remove();\n backProjection = undefined;\n if (backSource) {\n backSource.removeAttribute(marker);\n if (previousBackAriaHidden === null) backSource.removeAttribute('aria-hidden');\n else backSource.setAttribute('aria-hidden', previousBackAriaHidden);\n backSource.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n }\n backSource = undefined;\n previousBackAriaHidden = null;\n for (const { projection, actions } of toolbarProjections) {\n projection.remove();\n for (const { source, previousAriaHidden } of actions) {\n source.removeAttribute(marker);\n if (previousAriaHidden === null) source.removeAttribute('aria-hidden');\n else source.setAttribute('aria-hidden', previousAriaHidden);\n source.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n }\n }\n toolbarProjections = [];\n sourceObserver?.disconnect();\n sourceObserver = undefined;\n root?.classList.remove(readyClass);\n root = undefined;\n };\n const copyAttributes = (target: HTMLElement, original: HTMLElement) => {\n for (const name of ['id', 'slot', marker, 'aria-hidden']) target.removeAttribute(name);\n for (const attribute of Array.from(target.attributes))\n if (!['class', marker].includes(attribute.name) && !original.hasAttribute(attribute.name)) target.removeAttribute(attribute.name);\n for (const attribute of Array.from(original.attributes))\n if (!['id', 'slot', marker, 'aria-hidden'].includes(attribute.name)) target.setAttribute(attribute.name, attribute.value);\n target.classList.remove(prehiddenClass);\n };\n const syncBack = (target: HTMLIonBackButtonElement, original: HTMLIonBackButtonElement) => {\n copyAttributes(target, original);\n target.classList.add(backProjectionClass, 'ion-cloned-element');\n target.disabled = original.disabled;\n target.defaultHref = original.defaultHref;\n target.icon = original.icon;\n target.color = original.color;\n target.mode = original.mode;\n target.text = '';\n };\n const syncAction = (target: HTMLElement, original: HTMLElement) => {\n copyAttributes(target, original);\n target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');\n const label =\n original.getAttribute('aria-label') ??\n original.shadowRoot?.querySelector('[part~=native]')?.getAttribute('aria-label') ??\n original.textContent?.trim();\n if (label) target.setAttribute('aria-label', label);\n if ('disabled' in original) (target as HTMLIonButtonElement).disabled = (original as HTMLIonButtonElement).disabled;\n if (original.matches('ion-button')) {\n const icon = original.querySelector<HTMLElement>('ion-icon, svg');\n const clonedIcon = icon?.cloneNode(true) as HTMLElement | undefined;\n clonedIcon?.setAttribute('slot', 'icon-only');\n target.replaceChildren(...(clonedIcon ? [clonedIcon] : []));\n }\n };\n const sameSources = (back: HTMLIonBackButtonElement | undefined, groups: ToolbarSource[]) =>\n back === backSource &&\n groups.length === toolbarProjections.length &&\n groups.every(({ group, actions }, index) => {\n const current = toolbarProjections[index];\n return (\n group === current.source &&\n actions.length === current.actions.length &&\n actions.every((action, actionIndex) => action === current.actions[actionIndex].source)\n );\n });\n const syncExisting = () => {\n if (backSource && backProjection) syncBack(backProjection, backSource);\n let topOffset = backSource ? toolbarControlSize + toolbarControlGap : 0;\n for (const { projection, source, actions } of toolbarProjections) {\n if (actions.length === 1 && !projection.matches('ion-buttons')) syncAction(projection, actions[0].source);\n else copyAttributes(projection, source);\n projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');\n projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);\n if (projection.matches('ion-buttons')) actions.forEach(({ source: action, projection: clone }) => syncAction(clone, action));\n topOffset += actions.length * toolbarControlSize + toolbarControlGap;\n }\n };\n const project = (nextBack: HTMLIonBackButtonElement | undefined, groups: ToolbarSource[]) => {\n root = topModal(doc) ?? verticalBarsRoot()!;\n let topOffset = 0;\n if (nextBack) {\n backSource = nextBack;\n previousBackAriaHidden = nextBack.getAttribute('aria-hidden');\n backProjection = nextBack.cloneNode(false) as HTMLIonBackButtonElement;\n syncBack(backProjection, nextBack);\n backProjection.addEventListener(\n 'click',\n (event) => {\n event.preventDefault();\n event.stopImmediatePropagation();\n if (backSource && backSource === findBack()) activateProjectedElement(backSource);\n },\n { capture: true },\n );\n root.append(backProjection);\n nextBack.setAttribute(marker, '');\n nextBack.setAttribute('aria-hidden', 'true');\n nextBack.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n topOffset = toolbarControlSize + toolbarControlGap;\n }\n for (const { group, actions: sources } of groups) {\n const projection = (sources.length === 1 ? sources[0] : group).cloneNode(false) as HTMLElement;\n if (sources.length === 1) syncAction(projection, sources[0]);\n else copyAttributes(projection, group);\n projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');\n projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);\n const actions = sources.map((source) => {\n const clone = sources.length === 1 ? projection : (source.cloneNode(false) as HTMLElement);\n if (sources.length > 1) syncAction(clone, source);\n clone.addEventListener(\n 'click',\n (event) => {\n event.preventDefault();\n event.stopImmediatePropagation();\n if (isCurrentToolbarAction(source)) activateProjectedElement(source);\n },\n { capture: true },\n );\n if (sources.length > 1) projection.append(clone);\n const previousAriaHidden = source.getAttribute('aria-hidden');\n source.setAttribute(marker, '');\n source.setAttribute('aria-hidden', 'true');\n source.dispatchEvent(new CustomEvent('nativeUIShellChange'));\n return { source, projection: clone, previousAriaHidden };\n });\n root.append(projection);\n toolbarProjections.push({ source: group, projection, actions });\n topOffset += actions.length * toolbarControlSize + toolbarControlGap;\n }\n if (nextBack || toolbarProjections.length) root.classList.add(readyClass);\n sourceObserver = new MutationObserver((records) => {\n if (\n records.some(\n (record) =>\n record.type !== 'attributes' || (![marker, 'aria-hidden'].includes(record.attributeName ?? '') && !prehideOnlyMutation(record)),\n )\n )\n schedule();\n });\n if (nextBack?.shadowRoot)\n sourceObserver.observe(nextBack.shadowRoot, { subtree: true, childList: true, attributes: true, attributeOldValue: true });\n for (const { group } of groups)\n sourceObserver.observe(group, { subtree: true, childList: true, attributes: true, attributeOldValue: true });\n };\n const performUpdate = () => {\n frame = 0;\n const currentRoot = verticalBarsRoot();\n if (observingVerticalBars !== !!currentRoot) {\n observingVerticalBars = !!currentRoot;\n observer.disconnect();\n observer.observe(doc.documentElement, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeOldValue: true,\n attributeFilter: observingVerticalBars ? undefined : ['class'],\n });\n }\n if (stopped || suspended || !currentRoot || !enabled()) return restore();\n const nextBack = toolbarEnabled ? findBack() : undefined;\n const groups = toolbarEnabled ? findToolbarGroups() : [];\n const projectionRoot = topModal(doc) ?? currentRoot;\n search.update(projectionRoot);\n if (!nextBack && !groups.length && !search.source) return restore();\n if (root === (topModal(doc) ?? currentRoot) && sameSources(nextBack, groups)) return syncExisting();\n restore();\n project(nextBack, groups);\n search.update(projectionRoot);\n updates++;\n };\n const update = () => {\n try {\n performUpdate();\n } catch {\n restore();\n } finally {\n const pending = waiters;\n waiters = [];\n pending.forEach((resolve) => resolve());\n }\n };\n const schedule = (): Promise<void> => {\n if (stopped) return Promise.resolve();\n // Release Web ownership before the native runtime can acknowledge its next frame.\n if (!enabled()) restore();\n const done = new Promise<void>((resolve) => waiters.push(resolve));\n if (!frame) frame = win.requestAnimationFrame(update);\n return done;\n };\n const observer = new MutationObserver((records) => {\n const insideProjection = (target: Node) => {\n const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;\n const rootNode = target.getRootNode();\n const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;\n return !![element, shadowHost].some((candidate) =>\n candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}, .${searchProjectionClass}`),\n );\n };\n const verticalBarsChanged = records.some(\n (record) =>\n (record.type === 'attributes' && (record.target as Element).matches('ion-app')) ||\n (record.type === 'childList' &&\n Array.from(record.addedNodes).some(\n (node) =>\n node instanceof Element &&\n (node.matches('ion-app.ios-theme-vertical-bars') || !!node.querySelector('ion-app.ios-theme-vertical-bars')),\n )),\n );\n if (\n (observingVerticalBars &&\n records.some((record) => record.attributeName !== marker && !prehideOnlyMutation(record) && !insideProjection(record.target))) ||\n (!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record)))\n )\n schedule();\n });\n observer.observe(doc.documentElement, {\n subtree: true,\n childList: true,\n attributes: true,\n attributeOldValue: true,\n attributeFilter: ['class'],\n });\n const pageLifecycle = (event: Event) => {\n const page = event.target;\n if (!(page instanceof HTMLElement) || !page.matches('.ion-page')) return;\n if (event.type === VERTICAL_BARS_TRANSITION_CANCELED) {\n const entering = (event as CustomEvent<{ entering?: HTMLElement }>).detail?.entering;\n verticalBarsPages.cancel(entering, page);\n } else verticalBarsPages.lifecycle(event);\n if (verticalBarsRoot()) schedule();\n };\n for (const name of ['ionViewWillEnter', 'ionViewWillLeave', 'ionViewDidEnter', 'ionViewDidLeave'])\n doc.addEventListener(name, pageLifecycle, { capture: true, signal: listeners.signal });\n doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });\n for (const name of ['ionModalWillPresent', 'ionModalDidDismiss'])\n doc.addEventListener(name, schedule, { capture: true, signal: listeners.signal });\n win.addEventListener('resize', schedule, { signal: listeners.signal });\n win.addEventListener('nativeUIShellRefresh', schedule, { signal: listeners.signal });\n schedule();\n\n return {\n getStatus: (): NativeUIShellStatus => ({\n state: stopped ? 'stopped' : 'web',\n projected:\n Number(!!backSource) + Number(!!search.source) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),\n updates,\n }),\n async suspend() {\n suspended++;\n restore();\n let resumed = false;\n return {\n async resume() {\n if (resumed) return;\n resumed = true;\n suspended = Math.max(0, suspended - 1);\n if (!suspended) await schedule();\n },\n };\n },\n async destroy() {\n if (stopped) return;\n stopped = true;\n win.cancelAnimationFrame(frame);\n frame = 0;\n waiters.forEach((resolve) => resolve());\n waiters = [];\n observer.disconnect();\n sourceObserver?.disconnect();\n listeners.abort();\n restore();\n },\n };\n};\n"],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ //#region src/tab-bar-searchable/dismiss-button.d.ts
2
+ export declare const addSearchDismissButton: (footer: HTMLElement) => void;
3
+ //#endregion
4
+ //# sourceMappingURL=dismiss-button.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismiss-button.d.ts","names":[],"sources":["../../src/tab-bar-searchable/dismiss-button.ts"],"mappings":";qBAGa,yBAA0B,QAAQ"}
@@ -0,0 +1,34 @@
1
+ import { close } from "ionicons/icons";
2
+ //#region src/tab-bar-searchable/dismiss-button.ts
3
+ /** Keep the application's original dismissal handler for the focused search layout. */
4
+ const addSearchDismissButton = (footer) => {
5
+ if (footer.querySelector(".ios-theme-searchable-dismiss")) return;
6
+ const original = footer.querySelector("ion-buttons[slot=start] ion-button");
7
+ if (!original) return;
8
+ const buttons = footer.ownerDocument.createElement("ion-buttons");
9
+ buttons.slot = "end";
10
+ buttons.style.display = "none";
11
+ buttons.classList.add("ios-theme-searchable-dismiss", "ios-theme-horizontal-only");
12
+ const button = footer.ownerDocument.createElement("ion-button");
13
+ button.fill = "default";
14
+ button.type = "button";
15
+ button.setAttribute("aria-label", original.getAttribute("aria-label") ?? "Close");
16
+ button.addEventListener("pointerdown", (event) => event.preventDefault());
17
+ button.addEventListener("click", () => original.click());
18
+ const icon = footer.ownerDocument.createElement("ion-icon");
19
+ icon.slot = "icon-only";
20
+ icon.icon = close;
21
+ button.append(icon);
22
+ buttons.append(button);
23
+ footer.addEventListener("focusin", (event) => {
24
+ if (event.target.closest("ion-searchbar")) footer.classList.add("ios-theme-searchable-editing");
25
+ });
26
+ footer.addEventListener("focusout", (event) => {
27
+ if (!footer.contains(event.relatedTarget)) footer.classList.remove("ios-theme-searchable-editing");
28
+ });
29
+ original.closest("ion-toolbar")?.append(buttons);
30
+ };
31
+ //#endregion
32
+ export { addSearchDismissButton };
33
+
34
+ //# sourceMappingURL=dismiss-button.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dismiss-button.js","names":[],"sources":["../../src/tab-bar-searchable/dismiss-button.ts"],"sourcesContent":["import { close } from 'ionicons/icons';\n\n/** Keep the application's original dismissal handler for the focused search layout. */\nexport const addSearchDismissButton = (footer: HTMLElement): void => {\n if (footer.querySelector('.ios-theme-searchable-dismiss')) return;\n const original = footer.querySelector<HTMLElement>('ion-buttons[slot=start] ion-button');\n if (!original) return;\n const buttons = footer.ownerDocument.createElement('ion-buttons');\n buttons.slot = 'end';\n // Remain hidden when the full theme is absent or opted out.\n buttons.style.display = 'none';\n buttons.classList.add('ios-theme-searchable-dismiss', 'ios-theme-horizontal-only');\n const button = footer.ownerDocument.createElement('ion-button');\n button.fill = 'default';\n button.type = 'button';\n button.setAttribute('aria-label', original.getAttribute('aria-label') ?? 'Close');\n // Keep the field focused until click; blur would hide this button before dismissal.\n button.addEventListener('pointerdown', (event) => event.preventDefault());\n button.addEventListener('click', () => original.click());\n const icon = footer.ownerDocument.createElement('ion-icon');\n icon.slot = 'icon-only';\n icon.icon = close;\n button.append(icon);\n buttons.append(button);\n // Keep the editing layout while focus moves from clear to the slotted close button.\n footer.addEventListener('focusin', (event) => {\n if ((event.target as HTMLElement).closest('ion-searchbar')) footer.classList.add('ios-theme-searchable-editing');\n });\n footer.addEventListener('focusout', (event) => {\n if (!footer.contains(event.relatedTarget as Node | null)) footer.classList.remove('ios-theme-searchable-editing');\n });\n original.closest('ion-toolbar')?.append(buttons);\n};\n"],"mappings":";;;AAGA,MAAa,0BAA0B,WAA8B;CACnE,IAAI,OAAO,cAAc,+BAA+B,GAAG;CAC3D,MAAM,WAAW,OAAO,cAA2B,oCAAoC;CACvF,IAAI,CAAC,UAAU;CACf,MAAM,UAAU,OAAO,cAAc,cAAc,aAAa;CAChE,QAAQ,OAAO;CAEf,QAAQ,MAAM,UAAU;CACxB,QAAQ,UAAU,IAAI,gCAAgC,2BAA2B;CACjF,MAAM,SAAS,OAAO,cAAc,cAAc,YAAY;CAC9D,OAAO,OAAO;CACd,OAAO,OAAO;CACd,OAAO,aAAa,cAAc,SAAS,aAAa,YAAY,KAAK,OAAO;CAEhF,OAAO,iBAAiB,gBAAgB,UAAU,MAAM,eAAe,CAAC;CACxE,OAAO,iBAAiB,eAAe,SAAS,MAAM,CAAC;CACvD,MAAM,OAAO,OAAO,cAAc,cAAc,UAAU;CAC1D,KAAK,OAAO;CACZ,KAAK,OAAO;CACZ,OAAO,OAAO,IAAI;CAClB,QAAQ,OAAO,MAAM;CAErB,OAAO,iBAAiB,YAAY,UAAU;EAC5C,IAAK,MAAM,OAAuB,QAAQ,eAAe,GAAG,OAAO,UAAU,IAAI,8BAA8B;CACjH,CAAC;CACD,OAAO,iBAAiB,aAAa,UAAU;EAC7C,IAAI,CAAC,OAAO,SAAS,MAAM,aAA4B,GAAG,OAAO,UAAU,OAAO,8BAA8B;CAClH,CAAC;CACD,SAAS,QAAQ,aAAa,CAAC,EAAE,OAAO,OAAO;AACjD"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":["TabBarSearchableFunction"],"sources":["../../src/tab-bar-searchable/index.ts"],"mappings":";;qBAuDa,yBACX,WAAW,aACX,cAAc,aACd,WAAW,gBACVA"}
1
+ {"version":3,"file":"index.d.ts","names":["TabBarSearchableFunction"],"sources":["../../src/tab-bar-searchable/index.ts"],"mappings":";;qBAwDa,yBACX,WAAW,aACX,cAAc,aACd,WAAW,gBACVA"}
@@ -1,4 +1,5 @@
1
1
  import { isNativeUIShell, registerNativeSearch, requestNativeSearch, suspendNativeUIShell } from "../native-integration/index.js";
2
+ import { addSearchDismissButton } from "./dismiss-button.js";
2
3
  import { createCloseButtonsAnimation, createEffectAnimation, createFabButtonAnimation, createSearchContainerAnimation, createTabBarAnimation } from "./animations/enter.js";
3
4
  import { createReverseCloseButtonsAnimation, createReverseEffectAnimation, createReverseFabButtonAnimation, createReverseSearchContainerAnimation, createReverseTabBarAnimation } from "./animations/leave.js";
4
5
  import { createAnimation } from "@ionic/core";
@@ -24,6 +25,9 @@ import { ANIMATION_DELAY_BASE, ANIMATION_DURATION, ANIMATION_EASING, TabBarSearc
24
25
  **/
25
26
  const attachTabBarSearchable = (ionTabBar, ionFabButton, ionFooter) => {
26
27
  if (!ionTabBar || !ionFabButton || !ionFooter) throw new Error("TabBarSearchable should be defined with props");
28
+ ionFooter.classList.add("ios-theme-searchable");
29
+ ionFabButton.closest("ion-fab")?.classList.add("ios-theme-searchable-trigger");
30
+ addSearchDismissButton(ionFooter);
27
31
  ionFooter.style.pointerEvents = "none";
28
32
  ionFooter.style.opacity = "0";
29
33
  ionFooter.querySelector("ion-buttons[slot=start]")?.classList.add("ios-theme-horizontal-only");
@@ -110,6 +114,8 @@ const enterEvent = async (event, ionTabBar, ionFabButton, ionFooter, verticalSea
110
114
  };
111
115
  const leaveEvent = async (event, searchableEventCache, ionTabBar, ionFabButton, ionFooter, verticalSearch) => {
112
116
  if (!event.target?.closest("ion-buttons[slot=start] ion-button")) throw throwErrorByFailedClickElement("ion-buttons[slot=start] ion-button");
117
+ const focused = ionFooter.ownerDocument.activeElement;
118
+ if (focused instanceof HTMLElement && ionFooter.contains(focused)) focused.blur();
113
119
  const references = getElementReferences(ionTabBar, ionFooter);
114
120
  const tabAnimations = verticalSearch ? [] : [createReverseTabBarAnimation(ionTabBar, references, searchableEventCache.elementSizes), createReverseEffectAnimation(references, searchableEventCache.elementSizes, searchableEventCache.colorSelected)];
115
121
  const searchContainerAnimation = createReverseSearchContainerAnimation(references, searchableEventCache.elementSizes);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["TabBarSearchableType"],"sources":["../../src/tab-bar-searchable/index.ts"],"sourcesContent":["import { createAnimation } from '@ionic/core';\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.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 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":";;;;;;;;;;;;;;;;;;;;;;;;AAuDA,MAAa,0BACX,WACA,cACA,cAC6B;CAC7B,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,WAClC,MAAM,IAAI,MAAM,+CAA+C;CAGjE,UAAU,MAAM,gBAAgB;CAChC,UAAU,MAAM,UAAU;CAE1B,UAAU,cAAc,yBAAyB,CAAC,EAAE,UAAU,IAAI,2BAA2B;CAC7F,MAAM,eAAe,qBAAqB,WAAW,cAAc,SAAS;CAG5E,IAAI;CACJ,IAAI;CAEJ,IAAI,cAAc;CAClB,IAAI,gBAAgB;CACpB,IAAI,iBAAiB;CACrB,IAAI;CAEJ,OAAO,OAAO,OAAc,SAA+B;EACzD,MAAM,WAAW,SAASA,uBAAqB,QAAQ,mBAAmB;EAC1E,IAAI,CAAE,MAAM,QAAwB,QAAQ,QAAQ,GAAG,MAAM,+BAA+B,QAAQ;EACpG,IAAI,SAASA,uBAAqB,SAAS,CAAC,eAAe,CAAC,sBAC1D,MAAM,IAAI,MAAM,2EAA2E;EAC7F,MAAM,iBAAiB;EACvB,MAAM,UAAU,EAAE;EAClB,IAAI,SAASA,uBAAqB,OAAO,gBAAA,cAAgB,gBAAgB,SAAS;EAClF,MAAM,WAAW,MAAM,oBAAoB,cAAc,SAASA,uBAAqB,KAAK;EAC5F,IAAI,YAAY,eAAe;EAC/B,IAAI,UAAU;GACZ,cAAc,SAASA,uBAAqB;GAC5C;EACF;EACA,cAAc;EACd,IAAI,SAASA,uBAAqB,SAAS,gBAEzC;EAEF,IAAI,SAASA,uBAAqB,OAAO;GACvC,iBAAiB,CAAC,CAAC,UAAU,QAAQ,iCAAiC;GACtE,IAAI,gBAAgB;IAElB,MAAM,wBAAwB;KAC5B,IAAI,UAAU,UAAU,SAAS,gBAAgB,KAAK,UAAU,QAAQ,iCAAiC,GACvG,UAAU,UAAU,OAAO,gBAAgB;IAE/C;IACA,uBAAuB,WAAW;IAClC,wBAAwB,IAAI,iBAAiB,eAAe;IAC5D,sBAAsB,QAAQ,WAAW;KAAE,YAAY;KAAM,iBAAiB,CAAC,OAAO;IAAE,CAAC;IACzF,gBAAgB;GAClB;GACA,iBAAA,eAAiB,MAAM,qBAAqB;IAC1C,GAAI,iBAAiB,CAAC,IAAI,CAAC,SAAS;IACpC;IACA,aAAa,QAAQ,SAAS,KAAK;GACrC,CAAC;GACD,IAAI;IACF,uBAAuB,MAAM,WAAW,OAAO,WAAW,cAAc,WAAW,cAAc;GACnG,SAAS,OAAO;IACd,uBAAuB,WAAW;IAClC,aAAa;IACb,eAAe,KAAA;IACf,MAAM;GACR;EACF,OAAO,IAAI,yBAAyB,KAAA,GAClC,IAAI;GACF,MAAM,WAAW,OAAO,sBAAsB,WAAW,cAAc,WAAW,cAAc;GAChG,uBAAuB,KAAA;EACzB,UAAU;GACR,uBAAuB,WAAW;GAClC,eAAe;GACf,eAAe,KAAA;EACjB;OAEA,MAAM,IAAI,MAAM,2EAA2E;CAE/F;AACF;AAEA,MAAM,aAAa,OACjB,OACA,WACA,cACA,WACA,mBACkC;CAClC,IAAI,CAAE,MAAM,QAAwB,QAAQ,gBAAgB,GAC1D,MAAM,+BAA+B,gBAAgB;CAGvD,MAAM,aAAa,qBAAqB,WAAW,SAAS;CAC5D,MAAM,QAAQ,gBAAgB,WAAW,cAAc,UAAU;CACjE,MAAM,gBAAgB,WAAW,oBAC7B,iBAAiB,WAAW,iBAAiB,CAAC,CAAC,iBAAiB,kBAAkB,CAAC,CAAC,KAAK,IACzF;CAEJ,IAAI,gBAAgB,WAAW,gBAAgB,aAAa,QAAQ,OAAO;CAC3E,MAAM,gBAAgB,iBAClB,CAAC,IACD,CAAC,sBAAsB,WAAW,YAAY,KAAK,GAAG,sBAAsB,YAAY,KAAK,CAAC;CAClG,MAAM,2BAA2B,+BAA+B,YAAY,KAAK;CACjF,MAAM,wBAAwB,4BAA4B,UAAU;CACpE,MAAM,qBAAqB,yBAAyB,YAAY;CAEhE,MAAM,gBAAgB,CAAC,CACpB,MAAM,oBAAoB,CAAC,CAC3B,SAAS,kBAAkB,CAAC,CAC5B,OAAO,gBAAgB,CAAC,CACxB,WAAW,SAAS,CAAC,CACrB,oBAAqB,UAAU,MAAM,gBAAgB,MAAO,CAAC,CAC7D,OAAO,WAAW,OAAO,GAAG,CAAC,CAC7B,aAAa;EAAC,GAAG;EAAe;EAAoB;EAA0B;CAAqB,CAAC,CAAC,CACrG,KAAK;CAER,OAAO;EACL,cAAc;EACd;CACF;AACF;AAEA,MAAM,aAAa,OACjB,OACA,sBACA,WACA,cACA,WACA,mBACkB;CAClB,IAAI,CAAE,MAAM,QAAwB,QAAQ,oCAAoC,GAC9E,MAAM,+BAA+B,oCAAoC;CAG3E,MAAM,aAAa,qBAAqB,WAAW,SAAS;CAE5D,MAAM,gBAAgB,iBAClB,CAAC,IACD,CACE,6BAA6B,WAAW,YAAY,qBAAqB,YAAY,GACrF,6BAA6B,YAAY,qBAAqB,cAAc,qBAAqB,aAAa,CAChH;CACJ,MAAM,2BAA2B,sCAAsC,YAAY,qBAAqB,YAAY;CACpH,MAAM,wBAAwB,mCAAmC,UAAU;CAC3E,MAAM,qBAAqB,gCAAgC,cAAc,qBAAqB,YAAY;CAE1G,MAAM,gBAAgB,CAAC,CACpB,MAAM,oBAAoB,CAAC,CAC3B,SAAS,kBAAkB,CAAC,CAC5B,OAAO,gBAAgB,CAAC,CACxB,WAAW,SAAS,CAAC,CACrB,oBAAqB,UAAU,MAAM,gBAAgB,MAAO,CAAC,CAC7D,OAAO,WAAW,KAAK,GAAG,CAAC,CAC3B,aAAa;EAAC,GAAG;EAAe;EAAoB;EAA0B;CAAqB,CAAC,CAAC,CACrG,KAAK;AACV"}
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":";;;;;;;;;;;;;;;;;;;;;;;;;AAwDA,MAAa,0BACX,WACA,cACA,cAC6B;CAC7B,IAAI,CAAC,aAAa,CAAC,gBAAgB,CAAC,WAClC,MAAM,IAAI,MAAM,+CAA+C;CAGjE,UAAU,UAAU,IAAI,sBAAsB;CAC9C,aAAa,QAAQ,SAAS,CAAC,EAAE,UAAU,IAAI,8BAA8B;CAC7E,uBAAuB,SAAS;CAChC,UAAU,MAAM,gBAAgB;CAChC,UAAU,MAAM,UAAU;CAE1B,UAAU,cAAc,yBAAyB,CAAC,EAAE,UAAU,IAAI,2BAA2B;CAC7F,MAAM,eAAe,qBAAqB,WAAW,cAAc,SAAS;CAG5E,IAAI;CACJ,IAAI;CAEJ,IAAI,cAAc;CAClB,IAAI,gBAAgB;CACpB,IAAI,iBAAiB;CACrB,IAAI;CAEJ,OAAO,OAAO,OAAc,SAA+B;EACzD,MAAM,WAAW,SAASA,uBAAqB,QAAQ,mBAAmB;EAC1E,IAAI,CAAE,MAAM,QAAwB,QAAQ,QAAQ,GAAG,MAAM,+BAA+B,QAAQ;EACpG,IAAI,SAASA,uBAAqB,SAAS,CAAC,eAAe,CAAC,sBAC1D,MAAM,IAAI,MAAM,2EAA2E;EAC7F,MAAM,iBAAiB;EACvB,MAAM,UAAU,EAAE;EAClB,IAAI,SAASA,uBAAqB,OAAO,gBAAA,cAAgB,gBAAgB,SAAS;EAClF,MAAM,WAAW,MAAM,oBAAoB,cAAc,SAASA,uBAAqB,KAAK;EAC5F,IAAI,YAAY,eAAe;EAC/B,IAAI,UAAU;GACZ,cAAc,SAASA,uBAAqB;GAC5C;EACF;EACA,cAAc;EACd,IAAI,SAASA,uBAAqB,SAAS,gBAEzC;EAEF,IAAI,SAASA,uBAAqB,OAAO;GACvC,iBAAiB,CAAC,CAAC,UAAU,QAAQ,iCAAiC;GACtE,IAAI,gBAAgB;IAElB,MAAM,wBAAwB;KAC5B,IAAI,UAAU,UAAU,SAAS,gBAAgB,KAAK,UAAU,QAAQ,iCAAiC,GACvG,UAAU,UAAU,OAAO,gBAAgB;IAE/C;IACA,uBAAuB,WAAW;IAClC,wBAAwB,IAAI,iBAAiB,eAAe;IAC5D,sBAAsB,QAAQ,WAAW;KAAE,YAAY;KAAM,iBAAiB,CAAC,OAAO;IAAE,CAAC;IACzF,gBAAgB;GAClB;GACA,iBAAA,eAAiB,MAAM,qBAAqB;IAC1C,GAAI,iBAAiB,CAAC,IAAI,CAAC,SAAS;IACpC;IACA,aAAa,QAAQ,SAAS,KAAK;GACrC,CAAC;GACD,IAAI;IACF,uBAAuB,MAAM,WAAW,OAAO,WAAW,cAAc,WAAW,cAAc;GACnG,SAAS,OAAO;IACd,uBAAuB,WAAW;IAClC,aAAa;IACb,eAAe,KAAA;IACf,MAAM;GACR;EACF,OAAO,IAAI,yBAAyB,KAAA,GAClC,IAAI;GACF,MAAM,WAAW,OAAO,sBAAsB,WAAW,cAAc,WAAW,cAAc;GAChG,uBAAuB,KAAA;EACzB,UAAU;GACR,uBAAuB,WAAW;GAClC,eAAe;GACf,eAAe,KAAA;EACjB;OAEA,MAAM,IAAI,MAAM,2EAA2E;CAE/F;AACF;AAEA,MAAM,aAAa,OACjB,OACA,WACA,cACA,WACA,mBACkC;CAClC,IAAI,CAAE,MAAM,QAAwB,QAAQ,gBAAgB,GAC1D,MAAM,+BAA+B,gBAAgB;CAGvD,MAAM,aAAa,qBAAqB,WAAW,SAAS;CAC5D,MAAM,QAAQ,gBAAgB,WAAW,cAAc,UAAU;CACjE,MAAM,gBAAgB,WAAW,oBAC7B,iBAAiB,WAAW,iBAAiB,CAAC,CAAC,iBAAiB,kBAAkB,CAAC,CAAC,KAAK,IACzF;CAEJ,IAAI,gBAAgB,WAAW,gBAAgB,aAAa,QAAQ,OAAO;CAC3E,MAAM,gBAAgB,iBAClB,CAAC,IACD,CAAC,sBAAsB,WAAW,YAAY,KAAK,GAAG,sBAAsB,YAAY,KAAK,CAAC;CAClG,MAAM,2BAA2B,+BAA+B,YAAY,KAAK;CACjF,MAAM,wBAAwB,4BAA4B,UAAU;CACpE,MAAM,qBAAqB,yBAAyB,YAAY;CAEhE,MAAM,gBAAgB,CAAC,CACpB,MAAM,oBAAoB,CAAC,CAC3B,SAAS,kBAAkB,CAAC,CAC5B,OAAO,gBAAgB,CAAC,CACxB,WAAW,SAAS,CAAC,CACrB,oBAAqB,UAAU,MAAM,gBAAgB,MAAO,CAAC,CAC7D,OAAO,WAAW,OAAO,GAAG,CAAC,CAC7B,aAAa;EAAC,GAAG;EAAe;EAAoB;EAA0B;CAAqB,CAAC,CAAC,CACrG,KAAK;CAER,OAAO;EACL,cAAc;EACd;CACF;AACF;AAEA,MAAM,aAAa,OACjB,OACA,sBACA,WACA,cACA,WACA,mBACkB;CAClB,IAAI,CAAE,MAAM,QAAwB,QAAQ,oCAAoC,GAC9E,MAAM,+BAA+B,oCAAoC;CAG3E,MAAM,UAAU,UAAU,cAAc;CACxC,IAAI,mBAAmB,eAAe,UAAU,SAAS,OAAO,GAAG,QAAQ,KAAK;CAChF,MAAM,aAAa,qBAAqB,WAAW,SAAS;CAE5D,MAAM,gBAAgB,iBAClB,CAAC,IACD,CACE,6BAA6B,WAAW,YAAY,qBAAqB,YAAY,GACrF,6BAA6B,YAAY,qBAAqB,cAAc,qBAAqB,aAAa,CAChH;CACJ,MAAM,2BAA2B,sCAAsC,YAAY,qBAAqB,YAAY;CACpH,MAAM,wBAAwB,mCAAmC,UAAU;CAC3E,MAAM,qBAAqB,gCAAgC,cAAc,qBAAqB,YAAY;CAE1G,MAAM,gBAAgB,CAAC,CACpB,MAAM,oBAAoB,CAAC,CAC3B,SAAS,kBAAkB,CAAC,CAC5B,OAAO,gBAAgB,CAAC,CACxB,WAAW,SAAS,CAAC,CACrB,oBAAqB,UAAU,MAAM,gBAAgB,MAAO,CAAC,CAC7D,OAAO,WAAW,KAAK,GAAG,CAAC,CAC3B,aAAa;EAAC,GAAG;EAAe;EAAoB;EAA0B;CAAqB,CAAC,CAAC,CACrG,KAAK;AACV"}
@@ -94,7 +94,7 @@ The native host accepts input only within native controls. Tab interaction and a
94
94
 
95
95
  Existing `attachTabBarSearchable(tabBar, fabButton, footer)` registrations automatically use native search when their bottom tab bar and glass search controls are supported. No new component option, route, native setup or page listener is required. Ordinary tab bars continue using `UITabBar`; horizontal searchable groups use a persistent `UITabBarController`, `UITab` / `UISearchTab`, and `UISearchController`. The original Capacitor WebView continues to render results and handle navigation.
96
96
 
97
- With vertical bars, the native rail keeps every tab and its selection. SwiftUI’s `searchable` and `searchToolbarBehavior(.minimize)` provide the system search button, field, dismissal control and transitions within the same native navigation surface. Apple determines their placement for the current device layout and focuses the native field when system search opens. Input, focus, clear and submit events use the same bridge as horizontal search. When native search is unavailable, the Web search presentation also preserves vertical tabs.
97
+ With vertical bars, the native rail keeps every tab and its selection. SwiftUI’s `searchable` and `searchToolbarBehavior(.minimize)` provide the system search button, field, dismissal control and transitions within the same native navigation surface. Apple determines their placement for the current device layout and focuses the native field when system search opens. Input, focus, clear and submit events use the same bridge as horizontal search. As soon as vertical bars are enabled, the Web projection places the registered search button above visible vertical tabs, or at the bottom of the rail when the tabs are hidden, and forwards activation to the original FAB. Native search replaces that Web projection when available. The Web search presentation also preserves vertical tabs.
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
 
@@ -195,6 +195,12 @@ private struct ShellVerticalBarsPage: View {
195
195
  var body: some View {
196
196
  NavigationStack {
197
197
  Color.clear.modifier(ShellVerticalBarsToolbarAdapter(model: model))
198
+ .toolbar {
199
+ if model.search?.configuration.available == true {
200
+ // Let the system adapt bottom-bar search to the vertical rail.
201
+ DefaultToolbarItem(kind: .search, placement: .bottomBar)
202
+ }
203
+ }
198
204
  }
199
205
  }
200
206
  }
@@ -347,6 +353,13 @@ final class ShellVerticalBarsController: ShellVerticalBarsControlling {
347
353
  let hit = super.hitTest(point, with: event)
348
354
  let inRail = railEdge == "left" ? point.x <= railWidth : point.x >= bounds.maxX - railWidth
349
355
  if inRail { return hit }
356
+ // Expanded system search leaves the rail; its field and close button
357
+ // must receive touches instead of passing them through to the WebView.
358
+ var ancestor = hit
359
+ while let view = ancestor, view !== self {
360
+ if view is UIControl { return hit }
361
+ ancestor = view.superview
362
+ }
350
363
  guard hit != nil, containsBarSurface(at: point) else { return nil }
351
364
  return hit
352
365
  }
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-beta.pr246.sha237333ceac21",
4
+ "version": "1.2.0",
5
5
  "description": "iOS27 Theme for Ionic Framework",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -202,7 +202,7 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
202
202
  const retainInactive = (candidate: Candidate, state?: SearchState, keepAvailable = false) => {
203
203
  // Keep ShellSearchController across page transitions instead of demoting to UITabBar.
204
204
  // While the searchable page is only blocked mid-transition (not yet hidden), keep
205
- // available:true so native chrome does not swap to ordinary idleBar over album content.
205
+ // available:true so the system search button stays available over album content.
206
206
  if (state?.last) inactive.push([candidate, { ...state.last, active: false, available: keepAvailable, focused: false }]);
207
207
  };
208
208
  for (const binding of bindings) {
@@ -226,7 +226,7 @@ export const createSearchSupport = (doc: Document, id: (element: Element) => str
226
226
  if (binding.active && existing && isCurrent(existing) && existing.last && !pageUnavailable) {
227
227
  const back = binding.footer.querySelector<HTMLIonButtonElement>('ion-buttons[slot=start] ion-button');
228
228
  // Close must stay eligible; otherwise fall through and demote instead of caching active.
229
- if (back && !back.disabled && !back.closest(excluded)) {
229
+ if (back && !back.disabled && !back.closest(excluded) && !existing.bar.closest(excluded)) {
230
230
  candidate.control.search = {
231
231
  ...existing.last,
232
232
  active: true,
@@ -0,0 +1,80 @@
1
+ import { getNativeSearchBindings, type NativeSearchBinding } from '../native-integration';
2
+ import { activateProjectedElement, marker, unprojected } from './shared/dom';
3
+
4
+ export const searchProjectionClass = 'ios-theme-vertical-bars-search-projection';
5
+
6
+ /** Web counterpart of the system search button above the vertical tabs, or at the rail bottom without visible tabs. */
7
+ export const createVerticalBarsWebSearch = (doc: Document, eligible: (element: HTMLElement) => boolean, refresh: () => void) => {
8
+ let binding: NativeSearchBinding | undefined;
9
+ let projection: HTMLIonFabButtonElement | undefined;
10
+ let container: HTMLIonFabElement | undefined;
11
+ const iconObserver = new MutationObserver(refresh);
12
+ let previousAriaHidden: string | null = null;
13
+ const restore = () => {
14
+ iconObserver.disconnect();
15
+ container?.remove();
16
+ container = undefined;
17
+ projection = undefined;
18
+ if (binding) {
19
+ binding.trigger.removeAttribute(marker);
20
+ if (previousAriaHidden === null) binding.trigger.removeAttribute('aria-hidden');
21
+ else binding.trigger.setAttribute('aria-hidden', previousAriaHidden);
22
+ }
23
+ binding = undefined;
24
+ };
25
+ return {
26
+ restore,
27
+ get source() {
28
+ return binding?.trigger;
29
+ },
30
+ update(root: HTMLElement) {
31
+ const next = unprojected(binding ? [binding.trigger] : [], () =>
32
+ getNativeSearchBindings(doc).find(({ trigger, footer }) => eligible(trigger) && eligible(footer)),
33
+ );
34
+ if (next !== binding || container?.parentElement !== root) restore();
35
+ if (!next) return;
36
+ if (!projection) {
37
+ binding = next;
38
+ previousAriaHidden = next.trigger.getAttribute('aria-hidden');
39
+ projection = next.trigger.cloneNode(true) as HTMLIonFabButtonElement;
40
+ for (const name of ['id', 'slot', marker, 'aria-hidden', 'style']) projection.removeAttribute(name);
41
+ container = doc.createElement('ion-fab');
42
+ container.setAttribute('mode', (next.trigger as HTMLIonFabButtonElement).mode ?? 'ios');
43
+ container.classList.add(searchProjectionClass, 'ion-cloned-element');
44
+ projection.addEventListener(
45
+ 'click',
46
+ (event) => {
47
+ event.preventDefault();
48
+ event.stopImmediatePropagation();
49
+ activateProjectedElement(next.trigger);
50
+ },
51
+ { capture: true },
52
+ );
53
+ container.append(projection);
54
+ root.append(container);
55
+ next.trigger.setAttribute(marker, '');
56
+ next.trigger.setAttribute('aria-hidden', 'true');
57
+ }
58
+ projection.setAttribute(
59
+ 'aria-label',
60
+ next.trigger.getAttribute('aria-label') ?? next.footer.querySelector('ion-searchbar')?.getAttribute('aria-label') ?? 'Search',
61
+ );
62
+ const sourceIcon = next.trigger.querySelector('ion-icon');
63
+ const icon = projection.querySelector('ion-icon');
64
+ if (sourceIcon?.shadowRoot) iconObserver.observe(sourceIcon.shadowRoot, { subtree: true, childList: true, attributes: true });
65
+ if (sourceIcon && icon) {
66
+ icon.icon = sourceIcon.icon;
67
+ icon.name = sourceIcon.name;
68
+ icon.src = sourceIcon.src;
69
+ }
70
+ container!.hidden = next.trigger.style.pointerEvents === 'none';
71
+ projection.disabled = (next.trigger as HTMLIonFabButtonElement).disabled;
72
+ const bounds = next.tabBar.getBoundingClientRect();
73
+ const tabs = bounds.width > 0 && bounds.height > 0 && getComputedStyle(next.tabBar).visibility === 'visible' ? bounds : undefined;
74
+ container!.style.left = tabs ? `${tabs.left + (tabs.width - 46) / 2}px` : '';
75
+ container!.style.top = tabs ? `${tabs.top - 56}px` : '';
76
+ container!.style.right = tabs ? 'auto' : '';
77
+ container!.style.bottom = tabs ? 'auto' : '';
78
+ },
79
+ };
80
+ };
@@ -1,3 +1,4 @@
1
+ import { createVerticalBarsWebSearch, searchProjectionClass } from './vertical-bars-web-search';
1
2
  import { inVerticalBarsSurface, topModal } from './shared/modal';
2
3
  import type { NativeUIShellHandle, NativeUIShellOptions, NativeUIShellStatus } from './definitions';
3
4
  import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
@@ -9,6 +10,7 @@ import {
9
10
  verticalBarsEnteringPage,
10
11
  verticalBarsToolbarActions,
11
12
  isExcluded,
13
+ isShellDisabled,
12
14
  isVerticalBarsToolbarGroup,
13
15
  preferredVerticalBarsBack,
14
16
  verticalBarsOwned,
@@ -42,12 +44,7 @@ export const createVerticalBarsWebProjection = (
42
44
  enabled: () => boolean = () => true,
43
45
  ): NativeUIShellHandle => {
44
46
  const win = doc.defaultView!;
45
- if (options.controls !== undefined && options.controls.toolbar !== true)
46
- return {
47
- getStatus: () => ({ state: 'web', projected: 0, updates: 0 }),
48
- suspend: async () => ({ resume: async () => {} }),
49
- destroy: async () => {},
50
- };
47
+ const toolbarEnabled = options.controls === undefined || options.controls.toolbar === true;
51
48
  let root: HTMLElement | undefined;
52
49
  let backSource: HTMLIonBackButtonElement | undefined;
53
50
  let backProjection: HTMLIonBackButtonElement | undefined;
@@ -64,7 +61,7 @@ export const createVerticalBarsWebProjection = (
64
61
  const listeners = new AbortController();
65
62
  const verticalBarsRoot = () => doc.querySelector<HTMLElement>('ion-app.ios-theme-vertical-bars');
66
63
  const projectedSources = () =>
67
- [backSource, ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source))].filter(
64
+ [backSource, search.source, ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source))].filter(
68
65
  (source): source is HTMLElement => !!source,
69
66
  );
70
67
  const isRendered = (element: HTMLElement) =>
@@ -73,6 +70,20 @@ export const createVerticalBarsWebProjection = (
73
70
  const rect = element.getBoundingClientRect();
74
71
  return element.isConnected && style.display !== 'none' && style.visibility === 'visible' && rect.width > 0 && rect.height > 0;
75
72
  });
73
+ const search = createVerticalBarsWebSearch(
74
+ doc,
75
+ (element) =>
76
+ (options.controls === undefined || options.controls.tabs === true) &&
77
+ !!verticalBarsRoot()?.contains(element) &&
78
+ inVerticalBarsSurface(element) &&
79
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
80
+ !isShellDisabled(element) &&
81
+ !verticalBarsPages.isDeparted(element) &&
82
+ isRendered(element),
83
+ () => {
84
+ void schedule();
85
+ },
86
+ );
76
87
  const inEligibleToolbar = (element: HTMLElement) =>
77
88
  !!verticalBarsRoot()?.contains(element) &&
78
89
  inVerticalBarsSurface(element) &&
@@ -108,6 +119,7 @@ export const createVerticalBarsWebProjection = (
108
119
  };
109
120
  const isCurrentToolbarAction = (source: HTMLElement) => findToolbarGroups().some(({ actions }) => actions.includes(source));
110
121
  const restore = () => {
122
+ search.restore();
111
123
  backProjection?.remove();
112
124
  backProjection = undefined;
113
125
  if (backSource) {
@@ -272,12 +284,15 @@ export const createVerticalBarsWebProjection = (
272
284
  });
273
285
  }
274
286
  if (stopped || suspended || !currentRoot || !enabled()) return restore();
275
- const nextBack = findBack();
276
- const groups = findToolbarGroups();
277
- if (!nextBack && !groups.length) return restore();
287
+ const nextBack = toolbarEnabled ? findBack() : undefined;
288
+ const groups = toolbarEnabled ? findToolbarGroups() : [];
289
+ const projectionRoot = topModal(doc) ?? currentRoot;
290
+ search.update(projectionRoot);
291
+ if (!nextBack && !groups.length && !search.source) return restore();
278
292
  if (root === (topModal(doc) ?? currentRoot) && sameSources(nextBack, groups)) return syncExisting();
279
293
  restore();
280
294
  project(nextBack, groups);
295
+ search.update(projectionRoot);
281
296
  updates++;
282
297
  };
283
298
  const update = () => {
@@ -304,7 +319,9 @@ export const createVerticalBarsWebProjection = (
304
319
  const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;
305
320
  const rootNode = target.getRootNode();
306
321
  const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;
307
- return !![element, shadowHost].some((candidate) => candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}`));
322
+ return !![element, shadowHost].some((candidate) =>
323
+ candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}, .${searchProjectionClass}`),
324
+ );
308
325
  };
309
326
  const verticalBarsChanged = records.some(
310
327
  (record) =>
@@ -351,7 +368,8 @@ export const createVerticalBarsWebProjection = (
351
368
  return {
352
369
  getStatus: (): NativeUIShellStatus => ({
353
370
  state: stopped ? 'stopped' : 'web',
354
- projected: Number(!!backSource) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),
371
+ projected:
372
+ Number(!!backSource) + Number(!!search.source) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),
355
373
  updates,
356
374
  }),
357
375
  async suspend() {
@@ -41,7 +41,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
41
41
  }
42
42
  }
43
43
  ion-footer ion-toolbar {
44
- --padding-start: var(--ios-theme-menu-width, var(--ios26-menu-width));
44
+ --padding-start: calc(var(--ios-theme-menu-width, var(--ios26-menu-width)) + var(--ios-theme-searchable-padding-start, 0px));
45
45
  }
46
46
  ion-tab-bar.ios:not(.ios-theme-disabled, .ios26-disabled):not(:where(.ios-theme-vertical-bars *)) {
47
47
  &[slot='bottom'] {
@@ -75,7 +75,7 @@ ion-split-pane.ios:not(.ios-theme-disabled, .ios26-disabled).split-pane-visible:
75
75
  }
76
76
  }
77
77
  ion-footer ion-toolbar {
78
- --padding-end: var(--ios-theme-menu-width, var(--ios26-menu-width));
78
+ --padding-end: calc(var(--ios-theme-menu-width, var(--ios26-menu-width)) + var(--ios-theme-searchable-padding-end, 0px));
79
79
  }
80
80
  }
81
81
  }
@@ -22,3 +22,5 @@
22
22
  @use 'components/ion-toggle';
23
23
  @use 'components/ion-toolbar';
24
24
  @use 'vertical-bars';
25
+
26
+ @use 'utils/searchable';