@rdlabo/ionic-theme-ios27 1.2.0-beta.pr247.sha7c9dedc408a0 → 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.
@@ -1 +1 @@
1
-
1
+ ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic))>ion-toolbar{--ios-theme-searchable-padding-start: 16px;--ios-theme-searchable-padding-end: 20px;--padding-start: var(--ios-theme-searchable-padding-start);--padding-end: var(--ios-theme-searchable-padding-end)}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) ion-buttons[slot=start],ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) .ios-theme-searchable-dismiss{margin-inline:0}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) ion-buttons[slot=start] ion-button,ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) .ios-theme-searchable-dismiss ion-button{width:48px;height:48px;--border-radius: 50%;margin:0}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)).ios-theme-searchable-editing>ion-toolbar{--ios-theme-searchable-padding-start: 4px;--ios-theme-searchable-padding-end: 8px}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)).ios-theme-searchable-editing ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic){padding-inline:8px 12px}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)).ios-theme-searchable-editing ion-buttons[slot=start]{display:none}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)).ios-theme-searchable-editing .ios-theme-searchable-dismiss{display:flex !important}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic){--border-radius: 24px;padding-inline:12px}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic) input.searchbar-input{min-height:48px;padding-inline-end:40px}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic) .searchbar-clear-button{position:absolute;inset-inline-end:12px;width:20px;height:20px;min-width:20px;min-height:20px;background:none;box-shadow:none;backdrop-filter:none}ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled,.ios26-disabled):has(ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic)) ion-searchbar.ios:not(.ios-theme-disabled,.ios26-disabled,.searchbar-classic) .searchbar-clear-button ion-icon{width:20px;height:20px}ion-fab.ios-theme-searchable-trigger{pointer-events:none}ion-fab.ios-theme-searchable-trigger>ion-fab-button{pointer-events:auto}
@@ -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":"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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/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.pr247.sha7c9dedc408a0",
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",
@@ -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';
@@ -0,0 +1,80 @@
1
+ // UISearchTab keeps a compact selected-tab button until editing starts.
2
+ ion-footer.ios.ios-theme-searchable:not(.ios-theme-disabled, .ios26-disabled):has(
3
+ ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled, .searchbar-classic)
4
+ ) {
5
+ > ion-toolbar {
6
+ --ios-theme-searchable-padding-start: 16px;
7
+ --ios-theme-searchable-padding-end: 20px;
8
+ --padding-start: var(--ios-theme-searchable-padding-start);
9
+ --padding-end: var(--ios-theme-searchable-padding-end);
10
+ }
11
+
12
+ ion-buttons[slot='start'],
13
+ .ios-theme-searchable-dismiss {
14
+ margin-inline: 0;
15
+
16
+ ion-button {
17
+ width: 48px;
18
+ height: 48px;
19
+ --border-radius: 50%;
20
+ margin: 0;
21
+ }
22
+ }
23
+
24
+ &.ios-theme-searchable-editing {
25
+ > ion-toolbar {
26
+ // Searchbar padding (8px) and container margin (4px) complete the 16px tab-bar inset.
27
+ --ios-theme-searchable-padding-start: 4px;
28
+ --ios-theme-searchable-padding-end: 8px;
29
+ }
30
+
31
+ ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled, .searchbar-classic) {
32
+ padding-inline: 8px 12px;
33
+ }
34
+
35
+ ion-buttons[slot='start'] {
36
+ display: none;
37
+ }
38
+
39
+ .ios-theme-searchable-dismiss {
40
+ display: flex !important;
41
+ }
42
+ }
43
+
44
+ ion-searchbar.ios:not(.ios-theme-disabled, .ios26-disabled, .searchbar-classic) {
45
+ --border-radius: 24px;
46
+ padding-inline: 12px;
47
+
48
+ input.searchbar-input {
49
+ min-height: 48px;
50
+ padding-inline-end: 40px;
51
+ }
52
+
53
+ // Clear edits the value; the separate close button dismisses without clearing.
54
+ .searchbar-clear-button {
55
+ position: absolute;
56
+ inset-inline-end: 12px;
57
+ width: 20px;
58
+ height: 20px;
59
+ min-width: 20px;
60
+ min-height: 20px;
61
+ background: none;
62
+ box-shadow: none;
63
+ backdrop-filter: none;
64
+
65
+ ion-icon {
66
+ width: 20px;
67
+ height: 20px;
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ // The hidden trigger's container must not cover the focused dismissal button.
74
+ ion-fab.ios-theme-searchable-trigger {
75
+ pointer-events: none;
76
+
77
+ > ion-fab-button {
78
+ pointer-events: auto;
79
+ }
80
+ }
@@ -0,0 +1,33 @@
1
+ import { close } from 'ionicons/icons';
2
+
3
+ /** Keep the application's original dismissal handler for the focused search layout. */
4
+ export const addSearchDismissButton = (footer: HTMLElement): void => {
5
+ if (footer.querySelector('.ios-theme-searchable-dismiss')) return;
6
+ const original = footer.querySelector<HTMLElement>('ion-buttons[slot=start] ion-button');
7
+ if (!original) return;
8
+ const buttons = footer.ownerDocument.createElement('ion-buttons');
9
+ buttons.slot = 'end';
10
+ // Remain hidden when the full theme is absent or opted out.
11
+ buttons.style.display = 'none';
12
+ buttons.classList.add('ios-theme-searchable-dismiss', 'ios-theme-horizontal-only');
13
+ const button = footer.ownerDocument.createElement('ion-button');
14
+ button.fill = 'default';
15
+ button.type = 'button';
16
+ button.setAttribute('aria-label', original.getAttribute('aria-label') ?? 'Close');
17
+ // Keep the field focused until click; blur would hide this button before dismissal.
18
+ button.addEventListener('pointerdown', (event) => event.preventDefault());
19
+ button.addEventListener('click', () => original.click());
20
+ const icon = footer.ownerDocument.createElement('ion-icon');
21
+ icon.slot = 'icon-only';
22
+ icon.icon = close;
23
+ button.append(icon);
24
+ buttons.append(button);
25
+ // Keep the editing layout while focus moves from clear to the slotted close button.
26
+ footer.addEventListener('focusin', (event) => {
27
+ if ((event.target as HTMLElement).closest('ion-searchbar')) footer.classList.add('ios-theme-searchable-editing');
28
+ });
29
+ footer.addEventListener('focusout', (event) => {
30
+ if (!footer.contains(event.relatedTarget as Node | null)) footer.classList.remove('ios-theme-searchable-editing');
31
+ });
32
+ original.closest('ion-toolbar')?.append(buttons);
33
+ };
@@ -1,4 +1,5 @@
1
1
  import { createAnimation } from '@ionic/core';
2
+ import { addSearchDismissButton } from './dismiss-button';
2
3
  import {
3
4
  ANIMATION_DELAY_BASE,
4
5
  ANIMATION_DURATION,
@@ -62,6 +63,9 @@ export const attachTabBarSearchable = (
62
63
  throw new Error('TabBarSearchable should be defined with props');
63
64
  }
64
65
  // Initialize
66
+ ionFooter.classList.add('ios-theme-searchable');
67
+ ionFabButton.closest('ion-fab')?.classList.add('ios-theme-searchable-trigger');
68
+ addSearchDismissButton(ionFooter);
65
69
  ionFooter.style.pointerEvents = 'none';
66
70
  ionFooter.style.opacity = '0';
67
71
  // Search dismissal belongs beside the Web search field, never in the vertical rail.
@@ -191,6 +195,8 @@ const leaveEvent = async (
191
195
  throw throwErrorByFailedClickElement('ion-buttons[slot=start] ion-button');
192
196
  }
193
197
 
198
+ const focused = ionFooter.ownerDocument.activeElement;
199
+ if (focused instanceof HTMLElement && ionFooter.contains(focused)) focused.blur();
194
200
  const references = getElementReferences(ionTabBar, ionFooter);
195
201
 
196
202
  const tabAnimations = verticalSearch