@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.
- 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/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/package.json +1 -1
- 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/tab-bar-searchable/dismiss-button.ts +33 -0
- package/src/tab-bar-searchable/index.ts +6 -0
|
@@ -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 @@
|
|
|
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/package.json
CHANGED
|
@@ -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
|
}
|
|
@@ -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
|