@rdlabo/ionic-theme-ios27 0.1.0-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/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/css/components/ion-action-sheet.css +1 -0
- package/dist/css/components/ion-alert.css +1 -0
- package/dist/css/components/ion-button.css +1 -0
- package/dist/css/components/ion-card.css +1 -0
- package/dist/css/components/ion-content.css +1 -0
- package/dist/css/components/ion-datetime.css +1 -0
- package/dist/css/components/ion-fab.css +1 -0
- package/dist/css/components/ion-inputs.css +1 -0
- package/dist/css/components/ion-list.css +1 -0
- package/dist/css/components/ion-loading.css +1 -0
- package/dist/css/components/ion-menu.css +1 -0
- package/dist/css/components/ion-modal.css +1 -0
- package/dist/css/components/ion-picker.css +1 -0
- package/dist/css/components/ion-popover.css +1 -0
- package/dist/css/components/ion-range.css +1 -0
- package/dist/css/components/ion-searchbar.css +1 -0
- package/dist/css/components/ion-segment.css +1 -0
- package/dist/css/components/ion-tabs.css +1 -0
- package/dist/css/components/ion-toast.css +1 -0
- package/dist/css/components/ion-toggle.css +1 -0
- package/dist/css/components/ion-toolbar.css +1 -0
- package/dist/css/default-variables.css +1 -0
- package/dist/css/ionic-theme-ios26-dark-always.css +1 -0
- package/dist/css/ionic-theme-ios26-dark-class.css +1 -0
- package/dist/css/ionic-theme-ios26-dark-system.css +1 -0
- package/dist/css/ionic-theme-ios26.css +1 -0
- package/dist/css/md-ion-list-inset.css +1 -0
- package/dist/css/md-remove-ios-class-effect.css +1 -0
- package/dist/css/utils/api.css +1 -0
- package/dist/css/utils/dark/ion-button.css +1 -0
- package/dist/css/utils/dark/ion-fab.css +1 -0
- package/dist/css/utils/dark/ion-segment.css +1 -0
- package/dist/css/utils/dark/ion-tabs.css +1 -0
- package/dist/css/utils/searchable.css +1 -0
- package/dist/css/utils/structured-list.css +1 -0
- package/dist/css/utils/theme-dark.css +1 -0
- package/dist/css/utils/translucent.css +1 -0
- package/dist/docs.json +234 -0
- package/dist/focus-controller/index.d.ts +6 -0
- package/dist/focus-controller/index.d.ts.map +1 -0
- package/dist/focus-controller/index.js +62 -0
- package/dist/focus-controller /index.d.ts +6 -0
- package/dist/focus-controller /index.d.ts.map +1 -0
- package/dist/focus-controller /index.js +62 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/popover/animations/ios.enter.d.ts +4 -0
- package/dist/popover/animations/ios.enter.d.ts.map +1 -0
- package/dist/popover/animations/ios.enter.js +97 -0
- package/dist/popover/animations/ios.leave.d.ts +3 -0
- package/dist/popover/animations/ios.leave.d.ts.map +1 -0
- package/dist/popover/animations/ios.leave.js +60 -0
- package/dist/popover/popover-interface.d.ts +38 -0
- package/dist/popover/popover-interface.d.ts.map +1 -0
- package/dist/popover/popover-interface.js +1 -0
- package/dist/popover/utils.d.ts +37 -0
- package/dist/popover/utils.d.ts.map +1 -0
- package/dist/popover/utils.js +377 -0
- package/dist/sheets-of-glass/animations.d.ts +8 -0
- package/dist/sheets-of-glass/animations.d.ts.map +1 -0
- package/dist/sheets-of-glass/animations.js +97 -0
- package/dist/sheets-of-glass/index.d.ts +3 -0
- package/dist/sheets-of-glass/index.d.ts.map +1 -0
- package/dist/sheets-of-glass/index.js +174 -0
- package/dist/sheets-of-glass/interfaces.d.ts +16 -0
- package/dist/sheets-of-glass/interfaces.d.ts.map +1 -0
- package/dist/sheets-of-glass/interfaces.js +1 -0
- package/dist/tab-bar-searchable/animations/enter.d.ts +8 -0
- package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -0
- package/dist/tab-bar-searchable/animations/enter.js +73 -0
- package/dist/tab-bar-searchable/animations/leave.d.ts +8 -0
- package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -0
- package/dist/tab-bar-searchable/animations/leave.js +66 -0
- package/dist/tab-bar-searchable/index.d.ts +4 -0
- package/dist/tab-bar-searchable/index.d.ts.map +1 -0
- package/dist/tab-bar-searchable/index.js +75 -0
- package/dist/tab-bar-searchable/interfaces.d.ts +41 -0
- package/dist/tab-bar-searchable/interfaces.d.ts.map +1 -0
- package/dist/tab-bar-searchable/interfaces.js +5 -0
- package/dist/tab-bar-searchable/utils.d.ts +12 -0
- package/dist/tab-bar-searchable/utils.d.ts.map +1 -0
- package/dist/tab-bar-searchable/utils.js +60 -0
- package/dist/transition/index.d.ts +19 -0
- package/dist/transition/index.d.ts.map +1 -0
- package/dist/transition/index.js +219 -0
- package/dist/transition/ios.transition.d.ts +5 -0
- package/dist/transition/ios.transition.d.ts.map +1 -0
- package/dist/transition/ios.transition.js +609 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +69 -0
- package/docs/e2e-testing.md +51 -0
- package/docs/eslint.md +73 -0
- package/docs/experimental-animation.md +85 -0
- package/docs/features.md +88 -0
- package/docs/feedback.md +65 -0
- package/docs/ios-18.md +16 -0
- package/docs/migration.md +94 -0
- package/docs/special-markup.md +145 -0
- package/docs/using-ion-item-group.md +47 -0
- package/package.json +63 -0
- package/src/focus-controller/index.ts +123 -0
- package/src/index.ts +33 -0
- package/src/popover/animations/ios.enter.ts +144 -0
- package/src/popover/animations/ios.leave.ts +76 -0
- package/src/popover/popover-interface.ts +44 -0
- package/src/popover/utils.ts +696 -0
- package/src/sheets-of-glass/animations.ts +120 -0
- package/src/sheets-of-glass/index.ts +214 -0
- package/src/sheets-of-glass/interfaces.ts +17 -0
- package/src/styles/components/ion-action-sheet.scss +85 -0
- package/src/styles/components/ion-alert.scss +66 -0
- package/src/styles/components/ion-button.scss +443 -0
- package/src/styles/components/ion-card.scss +7 -0
- package/src/styles/components/ion-content.scss +22 -0
- package/src/styles/components/ion-datetime.scss +9 -0
- package/src/styles/components/ion-fab.scss +104 -0
- package/src/styles/components/ion-inputs.scss +82 -0
- package/src/styles/components/ion-list.scss +44 -0
- package/src/styles/components/ion-loading.scss +11 -0
- package/src/styles/components/ion-menu.scss +110 -0
- package/src/styles/components/ion-modal.scss +42 -0
- package/src/styles/components/ion-picker.scss +4 -0
- package/src/styles/components/ion-popover.scss +33 -0
- package/src/styles/components/ion-range.scss +90 -0
- package/src/styles/components/ion-searchbar.scss +68 -0
- package/src/styles/components/ion-segment.scss +150 -0
- package/src/styles/components/ion-tabs.scss +118 -0
- package/src/styles/components/ion-toast.scss +35 -0
- package/src/styles/components/ion-toggle.scss +65 -0
- package/src/styles/components/ion-toolbar.scss +76 -0
- package/src/styles/default-variables.scss +38 -0
- package/src/styles/ionic-theme-ios26-dark-always.scss +6 -0
- package/src/styles/ionic-theme-ios26-dark-class.scss +6 -0
- package/src/styles/ionic-theme-ios26-dark-system.scss +8 -0
- package/src/styles/ionic-theme-ios26.scss +23 -0
- package/src/styles/md-ion-list-inset.scss +22 -0
- package/src/styles/md-remove-ios-class-effect.scss +14 -0
- package/src/styles/utils/api.scss +146 -0
- package/src/styles/utils/dark/ion-button.scss +58 -0
- package/src/styles/utils/dark/ion-fab.scss +18 -0
- package/src/styles/utils/dark/ion-segment.scss +12 -0
- package/src/styles/utils/dark/ion-tabs.scss +21 -0
- package/src/styles/utils/searchable.scss +0 -0
- package/src/styles/utils/structured-list.scss +170 -0
- package/src/styles/utils/theme-dark.scss +25 -0
- package/src/styles/utils/translucent.scss +95 -0
- package/src/tab-bar-searchable/animations/enter.ts +92 -0
- package/src/tab-bar-searchable/animations/leave.ts +89 -0
- package/src/tab-bar-searchable/index.ts +141 -0
- package/src/tab-bar-searchable/interfaces.ts +28 -0
- package/src/tab-bar-searchable/utils.ts +75 -0
- package/src/transition/index.ts +369 -0
- package/src/transition/ios.transition.ts +966 -0
- package/src/utils.ts +91 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Using ion-item-group
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Using `ion-item-group` in inset lists
|
|
6
|
+
|
|
7
|
+
Most Ionic markup works without changes. When an `ion-list` uses `inset="true"`, wrap its items in `ion-item-group` and keep `ion-list-header` outside the group.
|
|
8
|
+
|
|
9
|
+
The examples use framework-neutral Web Component markup. In React or Vue, use the equivalent component and property syntax.
|
|
10
|
+
|
|
11
|
+
```html
|
|
12
|
+
<ion-list inset="true">
|
|
13
|
+
<ion-list-header><ion-label>Connections</ion-label></ion-list-header>
|
|
14
|
+
<ion-item-group>
|
|
15
|
+
<ion-item>...</ion-item>
|
|
16
|
+
<ion-item>...</ion-item>
|
|
17
|
+
</ion-item-group>
|
|
18
|
+
</ion-list>
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
No wrapper is required for lists that do not use `inset="true"`.
|
|
22
|
+
|
|
23
|
+
[Check list structure with ESLint](./eslint.md).
|
|
24
|
+
|
|
25
|
+
## Why the wrapper is required
|
|
26
|
+
|
|
27
|
+
Ionic normally gives `ion-list` its background, which makes `ion-list-header` appear inside the same surface as the items. The iOS 26 layout treats the header and item surface separately.
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
The theme therefore:
|
|
32
|
+
|
|
33
|
+
- makes the inset `ion-list` background transparent;
|
|
34
|
+
- applies the item surface to `ion-item-group`; and
|
|
35
|
+
- leaves `ion-list-header` outside that surface.
|
|
36
|
+
|
|
37
|
+
## Sharing the markup with Material Design
|
|
38
|
+
|
|
39
|
+
`@rdlabo/ionic-theme-md3` supports the same grouped markup, so one template can be used for both Ionic modes.
|
|
40
|
+
|
|
41
|
+
When an application uses this package without `@rdlabo/ionic-theme-md3`, import the optional stylesheet to apply the same grouped layout in Material mode:
|
|
42
|
+
|
|
43
|
+
```css
|
|
44
|
+
@import '@rdlabo/ionic-theme-ios27/dist/css/md-ion-list-inset.css';
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
For two-line items and section-header groups, see [Special markup and classes](./special-markup.md).
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@rdlabo/ionic-theme-ios27",
|
|
3
|
+
"private": false,
|
|
4
|
+
"version": "0.1.0-0",
|
|
5
|
+
"description": "iOS27 Theme for Ionic Framework",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./dist/css/*": "./dist/css/*",
|
|
15
|
+
"./src/styles/*": "./src/styles/*"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"package.json",
|
|
21
|
+
"docs"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "npm run build:css && npm run build:ts",
|
|
25
|
+
"build:css": "rm -rf dist/css && sass src/styles:dist/css --style=compressed --no-source-map",
|
|
26
|
+
"build:ts": "tsc",
|
|
27
|
+
"build:demo": "npm run build && cd demo && npm install && npm run build -- --configuration=production",
|
|
28
|
+
"lint": "prettier --check \"./**/*.{scss,ts}\" && prettier --parser angular --check \"./**/*.html\"",
|
|
29
|
+
"fmt": "prettier --write \"./**/*.{scss,ts}\" && prettier --parser angular --write \"./**/*.html\"",
|
|
30
|
+
"release": "np --no-tests --no-publish",
|
|
31
|
+
"prepublishOnly": "npm run build"
|
|
32
|
+
},
|
|
33
|
+
"repository": {
|
|
34
|
+
"type": "git",
|
|
35
|
+
"url": "git+ssh://git@github.com/rdlabo-dev/ionic-theme-ios27.git"
|
|
36
|
+
},
|
|
37
|
+
"keywords": [
|
|
38
|
+
"ionic",
|
|
39
|
+
"framework",
|
|
40
|
+
"theme",
|
|
41
|
+
"css",
|
|
42
|
+
"ios27"
|
|
43
|
+
],
|
|
44
|
+
"author": "Masahiko Sakakibara <sakakibara@rdlabo.jp>",
|
|
45
|
+
"license": "MIT",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/rdlabo-dev/ionic-theme-ios27/issues"
|
|
48
|
+
},
|
|
49
|
+
"homepage": "https://docs.rdlabo.dev/projects/ionic-theme-ios26",
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@ionic/angular": "^9.0.0",
|
|
52
|
+
"husky": "^9.1.7",
|
|
53
|
+
"lint-staged": "^16.2.1",
|
|
54
|
+
"np": "^10.2.0",
|
|
55
|
+
"prettier": "^3.6.2",
|
|
56
|
+
"sass": "^1.93.2",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"wrangler": "^4.125.0"
|
|
59
|
+
},
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@ionic/core": ">=8.8.1 <10"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { config } from '../utils';
|
|
2
|
+
// import { printIonWarning} from '@ionic/core/dist/types/utils/logging';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Moves focus to a specified element. Note that we do not remove the tabindex
|
|
6
|
+
* because that can result in an unintentional blur. Non-focusables can't be
|
|
7
|
+
* focused, so the body will get focused again.
|
|
8
|
+
*/
|
|
9
|
+
const moveFocus = (el: HTMLElement) => {
|
|
10
|
+
el.tabIndex = -1;
|
|
11
|
+
el.focus();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Elements that are hidden using `display: none` should not be focused even if
|
|
16
|
+
* they are present in the DOM.
|
|
17
|
+
*/
|
|
18
|
+
const isVisible = (el: HTMLElement) => {
|
|
19
|
+
return el.offsetParent !== null;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The focus controller allows us to manage focus within a view so assistive
|
|
24
|
+
* technologies can inform users of changes to the navigation state. Traditional
|
|
25
|
+
* native apps have a way of informing assistive technology about a navigation
|
|
26
|
+
* state change. Mobile browsers have this too, but only when doing a full page
|
|
27
|
+
* load. In a single page app we do not do that, so we need to build this
|
|
28
|
+
* integration ourselves.
|
|
29
|
+
*/
|
|
30
|
+
export const createFocusController = (): FocusController => {
|
|
31
|
+
const saveViewFocus = (referenceEl?: HTMLElement) => {
|
|
32
|
+
const focusManagerEnabled = config.get('focusManagerPriority', false);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* When going back to a previously visited page focus should typically be moved
|
|
36
|
+
* back to the element that was last focused when the user was on this view.
|
|
37
|
+
*/
|
|
38
|
+
if (focusManagerEnabled) {
|
|
39
|
+
const activeEl = document.activeElement;
|
|
40
|
+
if (activeEl !== null && referenceEl?.contains(activeEl)) {
|
|
41
|
+
activeEl.setAttribute(LAST_FOCUS, 'true');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const setViewFocus = (referenceEl: HTMLElement) => {
|
|
47
|
+
const focusManagerPriorities = config.get('focusManagerPriority', false);
|
|
48
|
+
/**
|
|
49
|
+
* If the focused element is a descendant of the referenceEl then it's possible
|
|
50
|
+
* that the app developer manually moved focus, so we do not want to override that.
|
|
51
|
+
* This can happen with inputs the are focused when a view transitions in.
|
|
52
|
+
*/
|
|
53
|
+
if (Array.isArray(focusManagerPriorities) && !referenceEl.contains(document.activeElement)) {
|
|
54
|
+
/**
|
|
55
|
+
* When going back to a previously visited view focus should always be moved back
|
|
56
|
+
* to the element that the user was last focused on when they were on this view.
|
|
57
|
+
*/
|
|
58
|
+
const lastFocus = referenceEl.querySelector<HTMLElement>(`[${LAST_FOCUS}]`);
|
|
59
|
+
if (lastFocus && isVisible(lastFocus)) {
|
|
60
|
+
moveFocus(lastFocus);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
for (const priority of focusManagerPriorities) {
|
|
65
|
+
/**
|
|
66
|
+
* For each recognized case (excluding the default case) make sure to return
|
|
67
|
+
* so that the fallback focus behavior does not run.
|
|
68
|
+
*
|
|
69
|
+
* We intentionally query for specific roles/semantic elements so that the
|
|
70
|
+
* transition manager can work with both Ionic and non-Ionic UI components.
|
|
71
|
+
*
|
|
72
|
+
* If new selectors are added, be sure to remove the outline ring by adding
|
|
73
|
+
* new selectors to rule in core.scss.
|
|
74
|
+
*/
|
|
75
|
+
switch (priority) {
|
|
76
|
+
case 'content':
|
|
77
|
+
const content = referenceEl.querySelector<HTMLElement>('main, [role="main"]');
|
|
78
|
+
if (content && isVisible(content)) {
|
|
79
|
+
moveFocus(content);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
case 'heading':
|
|
84
|
+
const headingOne = referenceEl.querySelector<HTMLElement>('h1, [role="heading"][aria-level="1"]');
|
|
85
|
+
if (headingOne && isVisible(headingOne)) {
|
|
86
|
+
moveFocus(headingOne);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
case 'banner':
|
|
91
|
+
const header = referenceEl.querySelector<HTMLElement>('header, [role="banner"]');
|
|
92
|
+
if (header && isVisible(header)) {
|
|
93
|
+
moveFocus(header);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
default:
|
|
98
|
+
// printIonWarning(`Unrecognized focus manager priority value ${priority}`);
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* If there is nothing to focus then focus the page so focus at least moves to
|
|
105
|
+
* the correct view. The browser will then determine where within the page to
|
|
106
|
+
* move focus to.
|
|
107
|
+
*/
|
|
108
|
+
moveFocus(referenceEl);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
saveViewFocus,
|
|
114
|
+
setViewFocus,
|
|
115
|
+
};
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export type FocusController = {
|
|
119
|
+
saveViewFocus: (referenceEl?: HTMLElement) => void;
|
|
120
|
+
setViewFocus: (referenceEl: HTMLElement) => void;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const LAST_FOCUS = 'ion-last-focus';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { registeredEffect } from './sheets-of-glass/interfaces';
|
|
2
|
+
import { registerEffect } from './sheets-of-glass';
|
|
3
|
+
export * from './sheets-of-glass/interfaces';
|
|
4
|
+
export { iosEnterAnimation as popoverEnterAnimation } from './popover/animations/ios.enter';
|
|
5
|
+
export { iosLeaveAnimation as popoverLeaveAnimation } from './popover/animations/ios.leave';
|
|
6
|
+
export * from './tab-bar-searchable';
|
|
7
|
+
export * from './transition/ios.transition';
|
|
8
|
+
|
|
9
|
+
export const registerTabBarEffect = (targetElement: HTMLElement): registeredEffect | undefined => {
|
|
10
|
+
return registerEffect(targetElement, 'ion-tab-button', 'tab-selected', {
|
|
11
|
+
small: 'scale(1.1, 1)',
|
|
12
|
+
medium: 'scale(1.2)',
|
|
13
|
+
large: 'scale(1.3)',
|
|
14
|
+
xlarge: 'scale(1.15, 1.4)',
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const registerSegmentEffect = (targetElement: HTMLElement): registeredEffect | undefined => {
|
|
19
|
+
const scale = !targetElement.classList.contains('segment-expand')
|
|
20
|
+
? {
|
|
21
|
+
small: 'scale(1.35)',
|
|
22
|
+
medium: 'scale(1.45)',
|
|
23
|
+
large: 'scale(1.55)',
|
|
24
|
+
xlarge: 'scale(1.55, 1.65)',
|
|
25
|
+
}
|
|
26
|
+
: {
|
|
27
|
+
small: 'scale(1.02, 1.35)',
|
|
28
|
+
medium: 'scale(1.03, 1.45)',
|
|
29
|
+
large: 'scale(1.04, 1.55)',
|
|
30
|
+
xlarge: 'scale(1.05, 1.65)',
|
|
31
|
+
};
|
|
32
|
+
return registerEffect(targetElement, 'ion-segment-button', 'segment-button-checked', scale);
|
|
33
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createAnimation } from '@ionic/core';
|
|
2
|
+
import type { Animation } from '@ionic/core';
|
|
3
|
+
import { getElementRoot } from '../../utils';
|
|
4
|
+
import { calculateWindowAdjustment, getPopoverDimensions, getPopoverPosition } from '../utils';
|
|
5
|
+
|
|
6
|
+
const POPOVER_IOS_BODY_PADDING = 5;
|
|
7
|
+
export const POPOVER_IOS_BODY_MARGIN = 8;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* iOS Popover Enter Animation
|
|
11
|
+
*/
|
|
12
|
+
// TODO(FW-2832): types
|
|
13
|
+
export const iosEnterAnimation = (baseEl: HTMLElement, opts?: any): Animation => {
|
|
14
|
+
const { event: ev, size, trigger, reference, side, align } = opts;
|
|
15
|
+
const doc = baseEl.ownerDocument as any;
|
|
16
|
+
const isRTL = doc.dir === 'rtl';
|
|
17
|
+
const bodyWidth = doc.defaultView.innerWidth;
|
|
18
|
+
const bodyHeight = doc.defaultView.innerHeight;
|
|
19
|
+
|
|
20
|
+
const root = getElementRoot(baseEl);
|
|
21
|
+
const contentEl = root.querySelector('.popover-content') as HTMLElement;
|
|
22
|
+
|
|
23
|
+
const referenceSizeEl = trigger || ev?.detail?.ionShadowTarget || ev?.target;
|
|
24
|
+
const { contentWidth, contentHeight } = getPopoverDimensions(size, contentEl, referenceSizeEl);
|
|
25
|
+
|
|
26
|
+
const isReplace = ((): boolean => {
|
|
27
|
+
if (!['ion-button', 'ion-buttons'].includes(referenceSizeEl.localName)) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
if (referenceSizeEl.classList.contains('ios26-disabled')) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
return true;
|
|
34
|
+
})();
|
|
35
|
+
|
|
36
|
+
const defaultPosition = {
|
|
37
|
+
top: bodyHeight / 2 - contentHeight / 2,
|
|
38
|
+
left: bodyWidth / 2 - contentWidth / 2,
|
|
39
|
+
originX: isRTL ? 'right' : 'left',
|
|
40
|
+
originY: 'top',
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const results = getPopoverPosition(isRTL, contentWidth, contentHeight, reference, side, align, defaultPosition, trigger, ev);
|
|
44
|
+
|
|
45
|
+
const padding = size === 'cover' ? 0 : POPOVER_IOS_BODY_PADDING;
|
|
46
|
+
const margin = size === 'cover' ? 0 : POPOVER_IOS_BODY_MARGIN;
|
|
47
|
+
|
|
48
|
+
const { originX, originY, top, left, bottom, checkSafeAreaLeft, checkSafeAreaRight, addPopoverBottomClass } = calculateWindowAdjustment(
|
|
49
|
+
side,
|
|
50
|
+
results.top,
|
|
51
|
+
results.left,
|
|
52
|
+
padding,
|
|
53
|
+
bodyWidth,
|
|
54
|
+
bodyHeight,
|
|
55
|
+
contentWidth,
|
|
56
|
+
contentHeight,
|
|
57
|
+
margin,
|
|
58
|
+
results.originX,
|
|
59
|
+
results.originY,
|
|
60
|
+
results.referenceCoordinates,
|
|
61
|
+
referenceSizeEl.getBoundingClientRect(),
|
|
62
|
+
isReplace,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const baseAnimation = createAnimation();
|
|
66
|
+
const backdropAnimation = createAnimation();
|
|
67
|
+
const contentAnimation = createAnimation();
|
|
68
|
+
const targetAnimation = createAnimation();
|
|
69
|
+
|
|
70
|
+
backdropAnimation
|
|
71
|
+
.delay(100)
|
|
72
|
+
.duration(300)
|
|
73
|
+
.addElement(root.querySelector('ion-backdrop')!)
|
|
74
|
+
.fromTo('opacity', 0.01, 'var(--backdrop-opacity)')
|
|
75
|
+
.beforeStyles({
|
|
76
|
+
'pointer-events': 'none',
|
|
77
|
+
})
|
|
78
|
+
.afterClearStyles(['pointer-events']);
|
|
79
|
+
|
|
80
|
+
// In Chromium, if the wrapper animates, the backdrop filter doesn't work.
|
|
81
|
+
// The Chromium team stated that this behavior is expected and not a bug. The element animating opacity creates a backdrop root for the backdrop-filter.
|
|
82
|
+
// To get around this, instead of animating the wrapper, animate content.
|
|
83
|
+
// https://bugs.chromium.org/p/chromium/issues/detail?id=1148826
|
|
84
|
+
contentAnimation
|
|
85
|
+
.easing('cubic-bezier(0, 1, 0.22, 1)')
|
|
86
|
+
.delay(100)
|
|
87
|
+
.duration(400)
|
|
88
|
+
.addElement(root.querySelector('.popover-content')!)
|
|
89
|
+
.beforeStyles({ 'transform-origin': `${originY} ${originX}` })
|
|
90
|
+
.beforeAddWrite(() => {
|
|
91
|
+
/**
|
|
92
|
+
* 'transformOrigin' use for leave animation.
|
|
93
|
+
*/
|
|
94
|
+
root.querySelector<HTMLElement>('.popover-content')!.dataset['transformOrigin'] = `${originY} ${originX}`;
|
|
95
|
+
})
|
|
96
|
+
.fromTo('transform', 'scale(0)', 'scale(1)')
|
|
97
|
+
.fromTo('opacity', 0.01, 1);
|
|
98
|
+
|
|
99
|
+
if (isReplace) {
|
|
100
|
+
targetAnimation
|
|
101
|
+
.delay(0)
|
|
102
|
+
.duration(200)
|
|
103
|
+
.addElement(referenceSizeEl)
|
|
104
|
+
.beforeStyles({ 'transform-origin': `${originY} ${originX}` })
|
|
105
|
+
.beforeAddClass('ios26-replace-element')
|
|
106
|
+
.fromTo('transform', 'scale(1)', 'scale(1.05)')
|
|
107
|
+
.fromTo('opacity', 1, 0);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return baseAnimation
|
|
111
|
+
.easing('ease')
|
|
112
|
+
.delay(100)
|
|
113
|
+
.duration(100)
|
|
114
|
+
.beforeAddWrite(() => {
|
|
115
|
+
if (size === 'cover') {
|
|
116
|
+
baseEl.style.setProperty('--width', `${contentWidth}px`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (addPopoverBottomClass) {
|
|
120
|
+
baseEl.classList.add('popover-bottom');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (bottom !== undefined) {
|
|
124
|
+
contentEl.style.setProperty('bottom', `${bottom}px`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const safeAreaLeft = ' + var(--ion-safe-area-left, 0)';
|
|
128
|
+
const safeAreaRight = ' - var(--ion-safe-area-right, 0)';
|
|
129
|
+
|
|
130
|
+
let leftValue = `${left}px`;
|
|
131
|
+
|
|
132
|
+
if (checkSafeAreaLeft) {
|
|
133
|
+
leftValue = `${left}px${safeAreaLeft}`;
|
|
134
|
+
}
|
|
135
|
+
if (checkSafeAreaRight) {
|
|
136
|
+
leftValue = `${left}px${safeAreaRight}`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
contentEl.style.setProperty('top', `calc(${top}px + var(--offset-y, 0))`);
|
|
140
|
+
contentEl.style.setProperty('left', `calc(${leftValue} + var(--offset-x, 0))`);
|
|
141
|
+
contentEl.style.setProperty('transform-origin', `${originY} ${originX}`);
|
|
142
|
+
})
|
|
143
|
+
.addAnimation([backdropAnimation, contentAnimation, targetAnimation]);
|
|
144
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createAnimation } from '@ionic/core';
|
|
2
|
+
import { getElementRoot } from '../../utils';
|
|
3
|
+
|
|
4
|
+
import type { Animation } from '@ionic/core';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* iOS Popover Leave Animation
|
|
8
|
+
*/
|
|
9
|
+
export const iosLeaveAnimation = (baseEl: HTMLElement): Animation => {
|
|
10
|
+
const root = getElementRoot(baseEl);
|
|
11
|
+
const contentEl = root.querySelector('.popover-content') as HTMLElement;
|
|
12
|
+
const arrowEl = root.querySelector('.popover-arrow') as HTMLElement | null;
|
|
13
|
+
|
|
14
|
+
const baseAnimation = createAnimation();
|
|
15
|
+
const backdropAnimation = createAnimation();
|
|
16
|
+
const contentAnimation = createAnimation();
|
|
17
|
+
const targetAnimation = createAnimation();
|
|
18
|
+
|
|
19
|
+
const doc = baseEl.ownerDocument as any;
|
|
20
|
+
const replaceElement = doc.querySelector('.ios26-replace-element') as HTMLElement | null;
|
|
21
|
+
|
|
22
|
+
if (replaceElement) {
|
|
23
|
+
const ratio = contentEl.getBoundingClientRect().width / contentEl.getBoundingClientRect().height;
|
|
24
|
+
const scale = ratio > 1 ? `${Math.min(1.2, 1.05 * ratio)}, 1.05` : `1.05, ${Math.min(1.2, 1.05 * ratio)}`;
|
|
25
|
+
|
|
26
|
+
targetAnimation
|
|
27
|
+
.addElement(replaceElement)
|
|
28
|
+
.delay(100)
|
|
29
|
+
.duration(300)
|
|
30
|
+
.afterRemoveClass('ios26-replace-element')
|
|
31
|
+
.fromTo('transform', `scale(${scale})`, 'scale(1)')
|
|
32
|
+
.fromTo('opacity', 0, 0.9);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
backdropAnimation.addElement(root.querySelector('ion-backdrop')!).fromTo('opacity', 'var(--backdrop-opacity)', 0);
|
|
36
|
+
|
|
37
|
+
contentAnimation
|
|
38
|
+
.duration(400)
|
|
39
|
+
.easing('ease')
|
|
40
|
+
.addElement(root.querySelector('.popover-arrow')!)
|
|
41
|
+
.addElement(root.querySelector('.popover-content')!)
|
|
42
|
+
.fromTo('opacity', 0.99, 0);
|
|
43
|
+
|
|
44
|
+
const popoverContentDataset = root.querySelector<HTMLElement>('.popover-content')!.dataset['transformOrigin'];
|
|
45
|
+
if (popoverContentDataset) {
|
|
46
|
+
contentAnimation.beforeStyles({ 'transform-origin': popoverContentDataset }).afterAddRead(() => {
|
|
47
|
+
root.querySelector<HTMLElement>('.popover-content')!.dataset['transformOrigin'] = '';
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (replaceElement) {
|
|
51
|
+
contentAnimation.fromTo('transform', 'scale(1)', 'scale(0.55)');
|
|
52
|
+
} else {
|
|
53
|
+
contentAnimation.fromTo('transform', 'scale(1)', 'scale(0)');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return baseAnimation
|
|
58
|
+
.easing('ease')
|
|
59
|
+
.afterAddWrite(() => {
|
|
60
|
+
baseEl.style.removeProperty('--width');
|
|
61
|
+
baseEl.classList.remove('popover-bottom');
|
|
62
|
+
|
|
63
|
+
contentEl.style.removeProperty('top');
|
|
64
|
+
contentEl.style.removeProperty('left');
|
|
65
|
+
contentEl.style.removeProperty('bottom');
|
|
66
|
+
contentEl.style.removeProperty('transform-origin');
|
|
67
|
+
|
|
68
|
+
if (arrowEl) {
|
|
69
|
+
arrowEl.style.removeProperty('top');
|
|
70
|
+
arrowEl.style.removeProperty('left');
|
|
71
|
+
arrowEl.style.removeProperty('display');
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
.duration(300)
|
|
75
|
+
.addAnimation([backdropAnimation, contentAnimation, targetAnimation]);
|
|
76
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { AnimationBuilder, ComponentProps, ComponentRef, FrameworkDelegate, Mode, OverlayInterface } from '@ionic/core';
|
|
2
|
+
|
|
3
|
+
export interface PopoverInterface extends OverlayInterface {
|
|
4
|
+
present: (event?: MouseEvent | TouchEvent | PointerEvent) => Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface PopoverOptions<T extends ComponentRef = ComponentRef> {
|
|
8
|
+
component: T;
|
|
9
|
+
componentProps?: ComponentProps<T>;
|
|
10
|
+
showBackdrop?: boolean;
|
|
11
|
+
backdropDismiss?: boolean;
|
|
12
|
+
translucent?: boolean;
|
|
13
|
+
cssClass?: string | string[];
|
|
14
|
+
event?: Event;
|
|
15
|
+
delegate?: FrameworkDelegate;
|
|
16
|
+
animated?: boolean;
|
|
17
|
+
focusTrap?: boolean;
|
|
18
|
+
|
|
19
|
+
mode?: Mode;
|
|
20
|
+
keyboardClose?: boolean;
|
|
21
|
+
id?: string;
|
|
22
|
+
htmlAttributes?: { [key: string]: any };
|
|
23
|
+
|
|
24
|
+
enterAnimation?: AnimationBuilder;
|
|
25
|
+
leaveAnimation?: AnimationBuilder;
|
|
26
|
+
|
|
27
|
+
size?: PopoverSize;
|
|
28
|
+
dismissOnSelect?: boolean;
|
|
29
|
+
reference?: PositionReference;
|
|
30
|
+
side?: PositionSide;
|
|
31
|
+
alignment?: PositionAlign;
|
|
32
|
+
arrow?: boolean;
|
|
33
|
+
|
|
34
|
+
trigger?: string;
|
|
35
|
+
triggerAction?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export type PopoverSize = 'cover' | 'auto';
|
|
39
|
+
|
|
40
|
+
export type TriggerAction = 'click' | 'hover' | 'context-menu';
|
|
41
|
+
|
|
42
|
+
export type PositionReference = 'trigger' | 'event';
|
|
43
|
+
export type PositionSide = 'top' | 'right' | 'bottom' | 'left' | 'start' | 'end';
|
|
44
|
+
export type PositionAlign = 'start' | 'center' | 'end';
|