@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.
- package/dist/css/components/ion-menu.css +1 -1
- package/dist/css/ionic-theme-ios27.css +1 -1
- package/dist/css/utils/searchable.css +1 -1
- package/dist/css/vertical-bars.css +1 -1
- package/dist/native/components/searchable-tabs.js +1 -1
- package/dist/native/components/searchable-tabs.js.map +1 -1
- package/dist/native/vertical-bars-web-search.d.ts +9 -0
- package/dist/native/vertical-bars-web-search.d.ts.map +1 -0
- package/dist/native/vertical-bars-web-search.js +84 -0
- package/dist/native/vertical-bars-web-search.js.map +1 -0
- package/dist/native/vertical-bars-web.d.ts.map +1 -1
- package/dist/native/vertical-bars-web.js +20 -16
- package/dist/native/vertical-bars-web.js.map +1 -1
- package/dist/tab-bar-searchable/dismiss-button.d.ts +4 -0
- package/dist/tab-bar-searchable/dismiss-button.d.ts.map +1 -0
- package/dist/tab-bar-searchable/dismiss-button.js +34 -0
- package/dist/tab-bar-searchable/dismiss-button.js.map +1 -0
- package/dist/tab-bar-searchable/index.d.ts.map +1 -1
- package/dist/tab-bar-searchable/index.js +6 -0
- package/dist/tab-bar-searchable/index.js.map +1 -1
- package/docs/native-ui-shell.md +1 -1
- package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +13 -0
- package/package.json +1 -1
- package/src/native/components/searchable-tabs.ts +2 -2
- package/src/native/vertical-bars-web-search.ts +80 -0
- package/src/native/vertical-bars-web.ts +30 -12
- package/src/styles/components/ion-menu.scss +2 -2
- package/src/styles/ionic-theme-ios27.scss +2 -0
- package/src/styles/utils/searchable.scss +80 -0
- package/src/styles/vertical-bars.scss +32 -0
- package/src/tab-bar-searchable/dismiss-button.ts +33 -0
- 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 @@
|
|
|
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":";;
|
|
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":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":["TabBarSearchableType"],"sources":["../../src/tab-bar-searchable/index.ts"],"sourcesContent":["import { createAnimation } from '@ionic/core';\nimport { addSearchDismissButton } from './dismiss-button';\nimport {\n ANIMATION_DELAY_BASE,\n ANIMATION_DURATION,\n ANIMATION_EASING,\n getElementReferences,\n getElementSizes,\n throwErrorByFailedClickElement,\n type SearchableEventCache,\n type TabBarSearchableFunction,\n TabBarSearchableType,\n} from '@rdlabo/ionic-theme-utils';\nimport { isNativeUIShell, registerNativeSearch, requestNativeSearch, suspendNativeUIShell } from '../native-integration';\nimport {\n createCloseButtonsAnimation,\n createEffectAnimation,\n createFabButtonAnimation,\n createSearchContainerAnimation,\n createTabBarAnimation,\n} from './animations/enter';\nimport {\n createReverseCloseButtonsAnimation,\n createReverseEffectAnimation,\n createReverseFabButtonAnimation,\n createReverseSearchContainerAnimation,\n createReverseTabBarAnimation,\n} from './animations/leave';\n\nexport {\n TabBarSearchableType,\n type TabBarSearchableFunction,\n type SearchableEventCache,\n type ElementSizes,\n type ElementReferences,\n} from '@rdlabo/ionic-theme-utils';\n\n/**\n * <ion-fab vertical=\"bottom\" horizontal=\"end\" slot=\"fixed\">\n * <ion-fab-button (click)=\"present($event)\">\n * <ion-icon name=\"search\"></ion-icon>\n * </ion-fab-button>\n * </ion-fab>\n * <ion-footer [translucent]=\"true\">\n * <ion-toolbar>\n * <ion-buttons slot=\"start\">\n * <!-- ion-icon does not need `name` attribute. -->\n * <ion-button fill=\"default\"><ion-icon slot=\"icon-only\"></ion-icon>\n * </ion-button>\n * </ion-buttons>\n * <!-- User set `ionChange` or other events. -->\n * <ion-searchbar aria-label=\"Search\" (ionChange)=\"example($event)\"></ion-searchbar>\n * </ion-toolbar>\n * </ion-footer>\n **/\n\nexport const attachTabBarSearchable = (\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n): TabBarSearchableFunction => {\n if (!ionTabBar || !ionFabButton || !ionFooter) {\n throw new Error('TabBarSearchable should be defined with props');\n }\n // Initialize\n ionFooter.classList.add('ios-theme-searchable');\n ionFabButton.closest('ion-fab')?.classList.add('ios-theme-searchable-trigger');\n addSearchDismissButton(ionFooter);\n ionFooter.style.pointerEvents = 'none';\n ionFooter.style.opacity = '0';\n // Search dismissal belongs beside the Web search field, never in the vertical rail.\n ionFooter.querySelector('ion-buttons[slot=start]')?.classList.add('ios-theme-horizontal-only');\n const nativeSearch = registerNativeSearch(ionTabBar, ionFabButton, ionFooter);\n\n // Saved Params\n let searchableEventCache: SearchableEventCache | undefined;\n let resumeNative: (() => void) | undefined;\n // Leave is valid both while native Enter awaits its ack and after it completes.\n let nativeEntry = false;\n let nativeRequest = 0;\n let verticalSearch = false;\n let tabVisibilityObserver: MutationObserver | undefined;\n\n return async (event: Event, type: TabBarSearchableType) => {\n const selector = type === TabBarSearchableType.Enter ? 'ion-fab-button' : 'ion-buttons[slot=start] ion-button';\n if (!(event.target as HTMLElement)?.closest(selector)) throw throwErrorByFailedClickElement(selector);\n if (type === TabBarSearchableType.Leave && !nativeEntry && !searchableEventCache)\n throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');\n const hadNativeEntry = nativeEntry;\n const request = ++nativeRequest;\n if (type === TabBarSearchableType.Enter) nativeEntry ||= isNativeUIShell(ionFooter);\n const accepted = await requestNativeSearch(nativeSearch, type === TabBarSearchableType.Enter);\n if (request !== nativeRequest) return;\n if (accepted) {\n nativeEntry = type === TabBarSearchableType.Enter;\n return;\n }\n nativeEntry = false;\n if (type === TabBarSearchableType.Leave && hadNativeEntry) {\n // Forced native retirement already restored the closed Web presentation.\n return;\n }\n if (type === TabBarSearchableType.Enter) {\n verticalSearch = !!ionTabBar.closest('ion-app.ios-theme-vertical-bars');\n if (verticalSearch) {\n // Ionic hides horizontal tabs when the keyboard opens; keep this rail available.\n const keepTabsVisible = () => {\n if (ionTabBar.classList.contains('tab-bar-hidden') && ionTabBar.closest('ion-app.ios-theme-vertical-bars')) {\n ionTabBar.classList.remove('tab-bar-hidden');\n }\n };\n tabVisibilityObserver?.disconnect();\n tabVisibilityObserver = new MutationObserver(keepTabsVisible);\n tabVisibilityObserver.observe(ionTabBar, { attributes: true, attributeFilter: ['class'] });\n keepTabsVisible();\n }\n resumeNative ??= await suspendNativeUIShell([\n ...(verticalSearch ? [] : [ionTabBar]),\n ionFooter,\n ionFabButton.closest('ion-fab') ?? ionFabButton,\n ]);\n try {\n searchableEventCache = await enterEvent(event, ionTabBar, ionFabButton, ionFooter, verticalSearch);\n } catch (error) {\n tabVisibilityObserver?.disconnect();\n resumeNative();\n resumeNative = undefined;\n throw error;\n }\n } else if (searchableEventCache !== undefined) {\n try {\n await leaveEvent(event, searchableEventCache, ionTabBar, ionFabButton, ionFooter, verticalSearch);\n searchableEventCache = undefined;\n } finally {\n tabVisibilityObserver?.disconnect();\n resumeNative?.();\n resumeNative = undefined;\n }\n } else {\n throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');\n }\n };\n};\n\nconst enterEvent = async (\n event: Event,\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n verticalSearch: boolean,\n): Promise<SearchableEventCache> => {\n if (!(event.target as HTMLElement)?.closest('ion-fab-button')) {\n throw throwErrorByFailedClickElement('ion-fab-button');\n }\n\n const references = getElementReferences(ionTabBar, ionFooter);\n const sizes = getElementSizes(ionTabBar, ionFabButton, references);\n const colorSelected = references.selectedTabButton\n ? getComputedStyle(references.selectedTabButton).getPropertyValue('--color-selected').trim()\n : '';\n\n if (verticalSearch) references.closeButtonIcon.setAttribute('name', 'close');\n const tabAnimations = verticalSearch\n ? []\n : [createTabBarAnimation(ionTabBar, references, sizes), createEffectAnimation(references, sizes)];\n const searchContainerAnimation = createSearchContainerAnimation(references, sizes);\n const closeButtonsAnimation = createCloseButtonsAnimation(references);\n const fabButtonAnimation = createFabButtonAnimation(ionFabButton);\n\n await createAnimation()\n .delay(ANIMATION_DELAY_BASE)\n .duration(ANIMATION_DURATION)\n .easing(ANIMATION_EASING)\n .addElement(ionFooter)\n .afterAddWrite(() => (ionFooter.style.pointerEvents = 'auto'))\n .fromTo('opacity', '0.8', '1')\n .addAnimation([...tabAnimations, fabButtonAnimation, searchContainerAnimation, closeButtonsAnimation])\n .play();\n\n return {\n elementSizes: sizes,\n colorSelected,\n };\n};\n\nconst leaveEvent = async (\n event: Event,\n searchableEventCache: SearchableEventCache,\n ionTabBar: HTMLElement,\n ionFabButton: HTMLElement,\n ionFooter: HTMLElement,\n verticalSearch: boolean,\n): Promise<void> => {\n if (!(event.target as HTMLElement)?.closest('ion-buttons[slot=start] ion-button')) {\n throw throwErrorByFailedClickElement('ion-buttons[slot=start] ion-button');\n }\n\n const focused = ionFooter.ownerDocument.activeElement;\n if (focused instanceof HTMLElement && ionFooter.contains(focused)) focused.blur();\n const references = getElementReferences(ionTabBar, ionFooter);\n\n const tabAnimations = verticalSearch\n ? []\n : [\n createReverseTabBarAnimation(ionTabBar, references, searchableEventCache.elementSizes),\n createReverseEffectAnimation(references, searchableEventCache.elementSizes, searchableEventCache.colorSelected),\n ];\n const searchContainerAnimation = createReverseSearchContainerAnimation(references, searchableEventCache.elementSizes);\n const closeButtonsAnimation = createReverseCloseButtonsAnimation(references);\n const fabButtonAnimation = createReverseFabButtonAnimation(ionFabButton, searchableEventCache.elementSizes);\n\n await createAnimation()\n .delay(ANIMATION_DELAY_BASE)\n .duration(ANIMATION_DURATION)\n .easing(ANIMATION_EASING)\n .addElement(ionFooter)\n .afterAddWrite(() => (ionFooter.style.pointerEvents = 'none'))\n .fromTo('opacity', '1', '0')\n .addAnimation([...tabAnimations, fabButtonAnimation, searchContainerAnimation, closeButtonsAnimation])\n .play();\n};\n"],"mappings":"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"}
|
package/docs/native-ui-shell.md
CHANGED
|
@@ -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.
|
|
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
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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) =>
|
|
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:
|
|
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
|
}
|