@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,170 @@
|
|
|
1
|
+
$groups: ion-item-group, ion-reorder-group, ion-accordion-group, ion-radio-group;
|
|
2
|
+
|
|
3
|
+
@mixin support-text() {
|
|
4
|
+
ion-item {
|
|
5
|
+
&:has(ion-input.input-fill-outline),
|
|
6
|
+
&:has(ion-textarea.textarea-fill-outline),
|
|
7
|
+
&:has(ion-select:is([helpertext]:not([helpertext='']), [errortext]:not([errortext='']))),
|
|
8
|
+
&:has(ion-input .input-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))),
|
|
9
|
+
&:has(ion-textarea .textarea-bottom > :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty))) {
|
|
10
|
+
--inner-border-width: 0;
|
|
11
|
+
--inner-padding-bottom: 4px;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
ion-input .input-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))),
|
|
15
|
+
ion-textarea .textarea-bottom:not(:has(> :is(.helper-text:not(:empty), .error-text:not(:empty), .counter:not(:empty)))) {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@mixin layout($outer-note-inset, $group-border-radius) {
|
|
22
|
+
background: transparent;
|
|
23
|
+
|
|
24
|
+
ion-list-header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
|
|
28
|
+
ion-label {
|
|
29
|
+
font-size: 0.9rem;
|
|
30
|
+
margin: 8px 4px 4px;
|
|
31
|
+
|
|
32
|
+
&:not(.ion-color) {
|
|
33
|
+
color: var(--ion-color-medium-tint);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
ion-button {
|
|
38
|
+
margin-top: 8px;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ion-note {
|
|
42
|
+
font-size: 0.9rem;
|
|
43
|
+
margin: 8px 16px 4px 0;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
ion-item-group.item-group-header {
|
|
48
|
+
& > ion-item > ion-label {
|
|
49
|
+
width: 100%;
|
|
50
|
+
text-align: center;
|
|
51
|
+
padding: 32px 16px 16px;
|
|
52
|
+
transform: translateX(5px);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* ion-header.header-collapse-condenseを使って、スクロールによってHeaderを変化させるトリック
|
|
56
|
+
*/
|
|
57
|
+
ion-header.header-collapse-condense > ion-toolbar {
|
|
58
|
+
--min-height: 0;
|
|
59
|
+
visibility: hidden;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ion-icon {
|
|
63
|
+
font-size: 2.8rem;
|
|
64
|
+
border-radius: 20%;
|
|
65
|
+
padding: 6px;
|
|
66
|
+
|
|
67
|
+
&:not(.ion-color) {
|
|
68
|
+
color: #ffffff;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
h2 {
|
|
73
|
+
font-size: 1.2rem;
|
|
74
|
+
font-weight: bold;
|
|
75
|
+
margin-top: 8px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
ion-text {
|
|
79
|
+
font-size: 0.9rem;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
> :is(#{$groups}) {
|
|
85
|
+
display: block;
|
|
86
|
+
border-radius: $group-border-radius;
|
|
87
|
+
overflow: hidden;
|
|
88
|
+
|
|
89
|
+
&:has(> .radio-group-top) {
|
|
90
|
+
display: flex;
|
|
91
|
+
flex-direction: column;
|
|
92
|
+
border-radius: 0;
|
|
93
|
+
overflow: visible;
|
|
94
|
+
|
|
95
|
+
> .radio-group-top {
|
|
96
|
+
order: 1;
|
|
97
|
+
font-size: 0.9rem;
|
|
98
|
+
padding-block-start: 8px;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
> ion-item:first-of-type {
|
|
102
|
+
--border-radius: #{$group-border-radius} #{$group-border-radius} 0 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
> ion-item:last-of-type {
|
|
106
|
+
--border-radius: 0 0 #{$group-border-radius} #{$group-border-radius};
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
&:not(:first-of-type) {
|
|
111
|
+
margin-top: 16px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
ion-item {
|
|
115
|
+
&:has(> ion-label:not([slot])):has(> ion-note:not([slot])) {
|
|
116
|
+
&::part(container) {
|
|
117
|
+
flex-direction: column;
|
|
118
|
+
justify-content: center;
|
|
119
|
+
padding-top: 14px;
|
|
120
|
+
padding-bottom: 14px;
|
|
121
|
+
min-height: 64px;
|
|
122
|
+
}
|
|
123
|
+
& > ion-label,
|
|
124
|
+
& > ion-note {
|
|
125
|
+
width: auto;
|
|
126
|
+
align-self: flex-start;
|
|
127
|
+
}
|
|
128
|
+
& > ion-label {
|
|
129
|
+
margin: 0;
|
|
130
|
+
font-weight: 500;
|
|
131
|
+
font-size: 1.15rem;
|
|
132
|
+
line-height: 0.9rem;
|
|
133
|
+
}
|
|
134
|
+
& > ion-note {
|
|
135
|
+
font-size: 0.85rem;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ion-text[slot='end'] {
|
|
140
|
+
padding-left: 8px;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
&.item-disabled {
|
|
144
|
+
--detail-icon-opacity: 0.1;
|
|
145
|
+
opacity: 1;
|
|
146
|
+
|
|
147
|
+
& > * {
|
|
148
|
+
opacity: 0.4;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
ion-button[slot='end'] {
|
|
153
|
+
&.ion-align-self-end {
|
|
154
|
+
transform: translateY(-7px);
|
|
155
|
+
}
|
|
156
|
+
ion-icon[slot='icon-only'] {
|
|
157
|
+
font-size: 1.2rem;
|
|
158
|
+
transform: translateY(4px);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
& > ion-note {
|
|
165
|
+
--color: var(--ion-color-medium-tint);
|
|
166
|
+
font-size: 0.9rem;
|
|
167
|
+
display: block;
|
|
168
|
+
margin: 8px calc(var(--ion-safe-area-right, 0) + $outer-note-inset) 8px calc(var(--ion-safe-area-left, 0) + $outer-note-inset);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
@forward 'dark/ion-button';
|
|
2
|
+
@use 'dark/ion-button' as *;
|
|
3
|
+
@forward 'dark/ion-fab';
|
|
4
|
+
@use 'dark/ion-fab' as *;
|
|
5
|
+
@forward 'dark/ion-tabs';
|
|
6
|
+
@use 'dark/ion-tabs' as *;
|
|
7
|
+
@forward 'dark/ion-segment';
|
|
8
|
+
@use 'dark/ion-segment' as *;
|
|
9
|
+
|
|
10
|
+
@mixin default-variables {
|
|
11
|
+
//--ios26-glass-background-rgb: 35, 35, 35;
|
|
12
|
+
--ios26-glass-background-rgb: 62, 62, 62;
|
|
13
|
+
--ios26-glass-box-shadow-color-rgb: 0, 0, 0 / 0;
|
|
14
|
+
--ios26-glass-border-color-rgb: 68, 68, 68;
|
|
15
|
+
--ios26-button-color-selected-rgb: 255, 255, 255;
|
|
16
|
+
|
|
17
|
+
--ios26-content-box-shadow-rgb: 0, 0, 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@mixin theme-dark {
|
|
21
|
+
@include ion-button;
|
|
22
|
+
@include ion-fab;
|
|
23
|
+
@include ion-tabs;
|
|
24
|
+
@include ion-segment;
|
|
25
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
@use 'api';
|
|
2
|
+
|
|
3
|
+
ion-header.ios:not(.ios26-disabled).header-translucent,
|
|
4
|
+
ion-footer.ios:not(.ios26-disabled).footer-translucent {
|
|
5
|
+
.header-background,
|
|
6
|
+
.footer-background {
|
|
7
|
+
backdrop-filter: none;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
ion-toolbar {
|
|
11
|
+
--border-width: 0;
|
|
12
|
+
--background: transparent;
|
|
13
|
+
--opacity: 1;
|
|
14
|
+
&::part(background) {
|
|
15
|
+
--opacity: 0;
|
|
16
|
+
}
|
|
17
|
+
ion-title {
|
|
18
|
+
transform: translateY(0);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
ion-header.ios:not(.ios26-disabled).header-translucent:not(.header-collapse-condense) {
|
|
24
|
+
&:has(> ion-toolbar.ios:not(.ios26-disabled) > *)::before {
|
|
25
|
+
height: 100%;
|
|
26
|
+
background: rgba(var(--ion-color-base-rgb, var(--ios26-content-box-shadow-rgb)), calc(0.2 * var(--opacity-scale, 1)));
|
|
27
|
+
backdrop-filter: blur(8px);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
&:not(.ion-no-border) > ion-toolbar.ios:not(.ios26-disabled):has(> *):last-of-type {
|
|
31
|
+
--border-width: 0 0 0.5px;
|
|
32
|
+
--border-color: var(--ion-toolbar-border-color, rgba(var(--ion-text-color-rgb, 0, 0, 0), 0.08));
|
|
33
|
+
&::part(background) {
|
|
34
|
+
--opacity: 1;
|
|
35
|
+
background: transparent;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ion-header.ios:not(.ios26-disabled).header-collapse-main ion-toolbar.in-toolbar ion-title {
|
|
41
|
+
transition:
|
|
42
|
+
opacity 0.5s ease-in-out,
|
|
43
|
+
color 0.5s ease-in-out,
|
|
44
|
+
text-shadow 0.5s ease-in-out,
|
|
45
|
+
transform 0.5s ease-in-out;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
ion-header.ios:not(.ios26-disabled).header-collapse-condense-inactive:not(.header-collapse-condense) ion-toolbar.in-toolbar ion-title {
|
|
49
|
+
color: transparent;
|
|
50
|
+
text-shadow: 0 0 1px var(--ion-color-base, var(--ion-toolbar-color, var(--ion-text-color, #000)));
|
|
51
|
+
transform: translateY(8px);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
ion-header.ios:not(.ios26-disabled).header-translucent::before,
|
|
55
|
+
ion-header.ios:not(.ios26-disabled).header-translucent::after,
|
|
56
|
+
ion-content.ios:not(.ios26-disabled).content-fullscreen::part(background)::before {
|
|
57
|
+
content: '';
|
|
58
|
+
position: absolute;
|
|
59
|
+
width: 100%;
|
|
60
|
+
pointer-events: none;
|
|
61
|
+
z-index: 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
ion-header.ios:not(.ios26-disabled).header-translucent::before {
|
|
65
|
+
/**
|
|
66
|
+
* With safe-area effect, this is need.
|
|
67
|
+
*/
|
|
68
|
+
z-index: 2;
|
|
69
|
+
top: 0;
|
|
70
|
+
height: var(--ion-safe-area-top);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ion-header.ios:not(.ios26-disabled).header-translucent::after {
|
|
74
|
+
z-index: 2;
|
|
75
|
+
top: 0;
|
|
76
|
+
height: 100%;
|
|
77
|
+
backdrop-filter: blur(2px);
|
|
78
|
+
mask-image: linear-gradient(rgba(0, 0, 0, 1) 80%, rgba(0, 0, 0, 0) 100%);
|
|
79
|
+
mask-composite: intersect;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* ここから下は、ion-content::beforeが content:'' を持たない限り表示されない
|
|
84
|
+
*/
|
|
85
|
+
.ion-page:has(> ion-header.header-translucent) > ion-content.content-fullscreen::part(background)::before {
|
|
86
|
+
z-index: 2;
|
|
87
|
+
top: 0;
|
|
88
|
+
height: var(--offset-top, 0px);
|
|
89
|
+
background: linear-gradient(
|
|
90
|
+
in oklab,
|
|
91
|
+
rgba(var(--ios26-content-box-shadow-rgb), 0.5) 0%,
|
|
92
|
+
rgba(var(--ios26-content-box-shadow-rgb), 0.2) 50%,
|
|
93
|
+
rgba(var(--ios26-content-box-shadow-rgb), 0) 100%
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ElementReferences, ElementSizes } from '../interfaces';
|
|
2
|
+
import { Animation, createAnimation } from '@ionic/core';
|
|
3
|
+
import { cloneElement } from '../../utils';
|
|
4
|
+
import { ANIMATION_DELAY_CLOSE_BUTTONS, OPACITY_TRANSITION } from '../utils';
|
|
5
|
+
|
|
6
|
+
export const createEffectAnimation = (references: ElementReferences, sizes: ElementSizes): Animation => {
|
|
7
|
+
const effectElement = cloneElement('ion-icon');
|
|
8
|
+
const closeButtonRect = references.closeButtonIcon?.getBoundingClientRect();
|
|
9
|
+
const iconName = references.selectedTabButtonIcon?.getAttribute('name');
|
|
10
|
+
const selectedTabButtonIconRect = references.selectedTabButtonIcon?.getBoundingClientRect();
|
|
11
|
+
|
|
12
|
+
return createAnimation()
|
|
13
|
+
.addElement(effectElement)
|
|
14
|
+
.beforeAddWrite(() => {
|
|
15
|
+
effectElement.style.display = 'inline-block';
|
|
16
|
+
references.closeButtonIcon.style.opacity = '0';
|
|
17
|
+
if (iconName && closeButtonRect) {
|
|
18
|
+
effectElement.setAttribute('name', iconName);
|
|
19
|
+
effectElement.style.width = `${closeButtonRect.width}px`;
|
|
20
|
+
effectElement.style.height = `${closeButtonRect.height}px`;
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
.afterAddWrite(() => {
|
|
24
|
+
effectElement.style.display = 'none';
|
|
25
|
+
references.closeButtonIcon.style.opacity = '1';
|
|
26
|
+
})
|
|
27
|
+
.fromTo(
|
|
28
|
+
'transform',
|
|
29
|
+
`translate3d(${selectedTabButtonIconRect.left}px, ${selectedTabButtonIconRect.top}px, 0)`,
|
|
30
|
+
`translate3d(${closeButtonRect.left}px, ${closeButtonRect.top}px, 0)`,
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const createSearchContainerAnimation = (references: ElementReferences, sizes: ElementSizes): Animation => {
|
|
35
|
+
return createAnimation()
|
|
36
|
+
.addElement(references.searchContainer)
|
|
37
|
+
.beforeAddWrite(() => (references.searchContainer.style.transformOrigin = 'right center'))
|
|
38
|
+
.fromTo(
|
|
39
|
+
'transform',
|
|
40
|
+
`scale(${sizes.fabButton.width / sizes.searchContainer.width}, ${sizes.fabButton.height / sizes.searchContainer.height})`,
|
|
41
|
+
'scale(1)',
|
|
42
|
+
)
|
|
43
|
+
.fromTo('opacity', '0.2', '1');
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const createCloseButtonsAnimation = (references: ElementReferences): Animation => {
|
|
47
|
+
return createAnimation()
|
|
48
|
+
.delay(ANIMATION_DELAY_CLOSE_BUTTONS)
|
|
49
|
+
.addElement(references.closeButtons)
|
|
50
|
+
.beforeAddWrite(() => (references.closeButtons.style.transformOrigin = 'left center'))
|
|
51
|
+
.afterClearStyles(['transform', 'transform-origin'])
|
|
52
|
+
.fromTo('transform', 'scale(1.5, 1)', 'scale(1)');
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const createTabBarAnimation = (ionTabBar: HTMLElement, references: ElementReferences, sizes: ElementSizes): Animation => {
|
|
56
|
+
return createAnimation()
|
|
57
|
+
.addElement(ionTabBar)
|
|
58
|
+
.beforeAddWrite(() => {
|
|
59
|
+
ionTabBar.querySelectorAll<HTMLElement>('ion-tab-button').forEach((element: HTMLElement) => {
|
|
60
|
+
element.style.transition = OPACITY_TRANSITION;
|
|
61
|
+
element.style.opacity = '0';
|
|
62
|
+
});
|
|
63
|
+
references.selectedTabButton.classList.remove('tab-selected');
|
|
64
|
+
const iconName = references.selectedTabButtonIcon?.getAttribute('name');
|
|
65
|
+
if (iconName) {
|
|
66
|
+
references.closeButtonIcon?.setAttribute('name', iconName);
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
.afterAddWrite(() => {
|
|
70
|
+
references.selectedTabButton.classList.add('tab-selected');
|
|
71
|
+
ionTabBar.style.pointerEvents = 'none';
|
|
72
|
+
})
|
|
73
|
+
.fromTo(
|
|
74
|
+
'transform',
|
|
75
|
+
'scale(1)',
|
|
76
|
+
`scale(${sizes.closeButton.width / sizes.tabBar.width}, ${sizes.closeButton.height / sizes.tabBar.height})`,
|
|
77
|
+
)
|
|
78
|
+
.fromTo('opacity', '1', '0');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const createFabButtonAnimation = (ionFabButton: HTMLElement): Animation => {
|
|
82
|
+
return createAnimation()
|
|
83
|
+
.addElement(ionFabButton)
|
|
84
|
+
.beforeAddWrite(() => {
|
|
85
|
+
ionFabButton.style.transformOrigin = 'center right';
|
|
86
|
+
ionFabButton.querySelector<HTMLElement>('ion-icon')?.style.setProperty('opacity', '0');
|
|
87
|
+
})
|
|
88
|
+
.afterAddWrite(() => {
|
|
89
|
+
ionFabButton.style.pointerEvents = 'none';
|
|
90
|
+
})
|
|
91
|
+
.fromTo('opacity', '1', '0');
|
|
92
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { ElementReferences, ElementSizes } from '../interfaces';
|
|
2
|
+
import { Animation, createAnimation } from '@ionic/core';
|
|
3
|
+
import { cloneElement } from '../../utils';
|
|
4
|
+
import { ANIMATION_DELAY_CLOSE_BUTTONS, OPACITY_TRANSITION } from '../utils';
|
|
5
|
+
|
|
6
|
+
export const createReverseEffectAnimation = (
|
|
7
|
+
references: ElementReferences,
|
|
8
|
+
searchableElementSizes: ElementSizes,
|
|
9
|
+
colorSelected: string,
|
|
10
|
+
): Animation => {
|
|
11
|
+
const effectElement = cloneElement('ion-icon');
|
|
12
|
+
const closeButtonRect = references.closeButtonIcon?.getBoundingClientRect();
|
|
13
|
+
|
|
14
|
+
return createAnimation()
|
|
15
|
+
.addElement(effectElement)
|
|
16
|
+
.beforeAddWrite(() => {
|
|
17
|
+
effectElement.style.display = 'inline-block';
|
|
18
|
+
references.closeButtonIcon.style.opacity = '0';
|
|
19
|
+
if (searchableElementSizes.selectedTabButtonIcon) {
|
|
20
|
+
effectElement.style.width = `${searchableElementSizes.selectedTabButtonIcon.width}px`;
|
|
21
|
+
effectElement.style.height = `${searchableElementSizes.selectedTabButtonIcon.height}px`;
|
|
22
|
+
effectElement.style.color = colorSelected;
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
.afterAddWrite(() => {
|
|
26
|
+
effectElement.style.display = 'none';
|
|
27
|
+
references.closeButtonIcon.style.opacity = '1';
|
|
28
|
+
effectElement.style.color = '';
|
|
29
|
+
})
|
|
30
|
+
.fromTo(
|
|
31
|
+
'transform',
|
|
32
|
+
`translate3d(${closeButtonRect.left}px, ${closeButtonRect.top}px, 0)`,
|
|
33
|
+
`translate3d(${searchableElementSizes.selectedTabButtonIcon.left}px, ${searchableElementSizes.selectedTabButtonIcon.top}px, 0)`,
|
|
34
|
+
);
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export const createReverseSearchContainerAnimation = (references: ElementReferences, sizes: ElementSizes): Animation => {
|
|
38
|
+
return createAnimation()
|
|
39
|
+
.addElement(references.searchContainer)
|
|
40
|
+
.beforeAddWrite(() => (references.searchContainer.style.transformOrigin = 'right center'))
|
|
41
|
+
.fromTo(
|
|
42
|
+
'transform',
|
|
43
|
+
'scale(1)',
|
|
44
|
+
`scale(${sizes.fabButton.width / sizes.searchContainer.width}, ${sizes.fabButton.height / sizes.searchContainer.height})`,
|
|
45
|
+
)
|
|
46
|
+
.fromTo('opacity', '1', '0.2');
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const createReverseCloseButtonsAnimation = (references: ElementReferences): Animation => {
|
|
50
|
+
return createAnimation()
|
|
51
|
+
.delay(ANIMATION_DELAY_CLOSE_BUTTONS)
|
|
52
|
+
.addElement(references.closeButtons)
|
|
53
|
+
.beforeAddWrite(() => (references.closeButtons.style.transformOrigin = 'left center'))
|
|
54
|
+
.afterClearStyles(['transform', 'transform-origin'])
|
|
55
|
+
.fromTo('transform', 'scale(1)', 'scale(1.5, 1)');
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export const createReverseTabBarAnimation = (ionTabBar: HTMLElement, references: ElementReferences, sizes: ElementSizes): Animation => {
|
|
59
|
+
return createAnimation()
|
|
60
|
+
.addElement(ionTabBar)
|
|
61
|
+
.beforeAddWrite(() => {
|
|
62
|
+
ionTabBar.style.pointerEvents = 'auto';
|
|
63
|
+
ionTabBar.querySelectorAll<HTMLElement>('ion-tab-button').forEach((element: HTMLElement) => {
|
|
64
|
+
element.style.transition = OPACITY_TRANSITION;
|
|
65
|
+
element.style.opacity = '1';
|
|
66
|
+
});
|
|
67
|
+
})
|
|
68
|
+
.afterClearStyles(['transform', 'opacity'])
|
|
69
|
+
.fromTo(
|
|
70
|
+
'transform',
|
|
71
|
+
`scale(${sizes.closeButton.width / sizes.tabBar.width}, ${sizes.closeButton.height / sizes.tabBar.height})`,
|
|
72
|
+
'scale(1)',
|
|
73
|
+
)
|
|
74
|
+
.fromTo('opacity', '0', '1');
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const createReverseFabButtonAnimation = (ionFabButton: HTMLElement, references: ElementSizes): Animation => {
|
|
78
|
+
return createAnimation()
|
|
79
|
+
.addElement(ionFabButton)
|
|
80
|
+
.beforeAddWrite(() => {
|
|
81
|
+
ionFabButton.querySelector<HTMLElement>('ion-icon')?.style.setProperty('opacity', '1');
|
|
82
|
+
ionFabButton.style.transformOrigin = 'center right';
|
|
83
|
+
})
|
|
84
|
+
.afterAddWrite(() => {
|
|
85
|
+
ionFabButton.style.pointerEvents = 'auto';
|
|
86
|
+
})
|
|
87
|
+
.fromTo('opacity', '0', '1')
|
|
88
|
+
.fromTo('transform', `scale(${references.searchContainer.height / references.fabButton.height})`, 'scale(1)');
|
|
89
|
+
};
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createAnimation } from '@ionic/core';
|
|
2
|
+
import {
|
|
3
|
+
ANIMATION_DELAY_BASE,
|
|
4
|
+
ANIMATION_DURATION,
|
|
5
|
+
ANIMATION_EASING,
|
|
6
|
+
getElementReferences,
|
|
7
|
+
getElementSizes,
|
|
8
|
+
throwErrorByFailedClickElement,
|
|
9
|
+
} from './utils';
|
|
10
|
+
import { SearchableEventCache, TabBarSearchableFunction, TabBarSearchableType } from './interfaces';
|
|
11
|
+
import {
|
|
12
|
+
createCloseButtonsAnimation,
|
|
13
|
+
createEffectAnimation,
|
|
14
|
+
createFabButtonAnimation,
|
|
15
|
+
createSearchContainerAnimation,
|
|
16
|
+
createTabBarAnimation,
|
|
17
|
+
} from './animations/enter';
|
|
18
|
+
import {
|
|
19
|
+
createReverseCloseButtonsAnimation,
|
|
20
|
+
createReverseEffectAnimation,
|
|
21
|
+
createReverseFabButtonAnimation,
|
|
22
|
+
createReverseSearchContainerAnimation,
|
|
23
|
+
createReverseTabBarAnimation,
|
|
24
|
+
} from './animations/leave';
|
|
25
|
+
|
|
26
|
+
export * from './interfaces';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* <ion-fab vertical="bottom" horizontal="end" slot="fixed">
|
|
30
|
+
* <ion-fab-button (click)="present($event)">
|
|
31
|
+
* <ion-icon name="search"></ion-icon>
|
|
32
|
+
* </ion-fab-button>
|
|
33
|
+
* </ion-fab>
|
|
34
|
+
* <ion-footer [translucent]="true">
|
|
35
|
+
* <ion-toolbar>
|
|
36
|
+
* <ion-buttons slot="start">
|
|
37
|
+
* <!-- ion-icon does not need `name` attribute. -->
|
|
38
|
+
* <ion-button fill="default"><ion-icon slot="icon-only"></ion-icon>
|
|
39
|
+
* </ion-button>
|
|
40
|
+
* </ion-buttons>
|
|
41
|
+
* <!-- User set `ionChange` or other events. -->
|
|
42
|
+
* <ion-searchbar (ionChange)="example($event)"></ion-searchbar>
|
|
43
|
+
* </ion-toolbar>
|
|
44
|
+
* </ion-footer>
|
|
45
|
+
**/
|
|
46
|
+
|
|
47
|
+
export const attachTabBarSearchable = (
|
|
48
|
+
ionTabBar: HTMLElement,
|
|
49
|
+
ionFabButton: HTMLElement,
|
|
50
|
+
ionFooter: HTMLElement,
|
|
51
|
+
): TabBarSearchableFunction => {
|
|
52
|
+
if (!ionTabBar || !ionFabButton || !ionFooter) {
|
|
53
|
+
throw new Error('TabBarSearchable should be defined with props');
|
|
54
|
+
}
|
|
55
|
+
// Initialize
|
|
56
|
+
ionFooter.style.pointerEvents = 'none';
|
|
57
|
+
ionFooter.style.opacity = '0';
|
|
58
|
+
ionTabBar.style.transformOrigin = 'left center';
|
|
59
|
+
|
|
60
|
+
// Saved Params
|
|
61
|
+
let searchableEventCache: SearchableEventCache | undefined;
|
|
62
|
+
|
|
63
|
+
return async (event: Event, type: TabBarSearchableType) => {
|
|
64
|
+
if (type === TabBarSearchableType.Enter) {
|
|
65
|
+
searchableEventCache = await enterEvent(event, ionTabBar, ionFabButton, ionFooter);
|
|
66
|
+
} else if (searchableEventCache !== undefined) {
|
|
67
|
+
await leaveEvent(event, searchableEventCache, ionTabBar, ionFabButton, ionFooter);
|
|
68
|
+
searchableEventCache = undefined;
|
|
69
|
+
} else {
|
|
70
|
+
throw new Error('TabBarSearchableType.Leave should be run after TabBarSearchableType.Enter');
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const enterEvent = async (
|
|
76
|
+
event: Event,
|
|
77
|
+
ionTabBar: HTMLElement,
|
|
78
|
+
ionFabButton: HTMLElement,
|
|
79
|
+
ionFooter: HTMLElement,
|
|
80
|
+
): Promise<SearchableEventCache> => {
|
|
81
|
+
if (!(event.target as HTMLElement)?.closest('ion-fab-button')) {
|
|
82
|
+
throw throwErrorByFailedClickElement('ion-fab-button');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const references = getElementReferences(ionTabBar, ionFooter);
|
|
86
|
+
const sizes = getElementSizes(ionTabBar, ionFabButton, references);
|
|
87
|
+
const colorSelected = references.selectedTabButton
|
|
88
|
+
? getComputedStyle(references.selectedTabButton).getPropertyValue('--color-selected').trim()
|
|
89
|
+
: '';
|
|
90
|
+
|
|
91
|
+
const effectAnimation = createEffectAnimation(references, sizes);
|
|
92
|
+
const searchContainerAnimation = createSearchContainerAnimation(references, sizes);
|
|
93
|
+
const closeButtonsAnimation = createCloseButtonsAnimation(references);
|
|
94
|
+
const tabBarAnimation = createTabBarAnimation(ionTabBar, references, sizes);
|
|
95
|
+
const fabButtonAnimation = createFabButtonAnimation(ionFabButton);
|
|
96
|
+
|
|
97
|
+
await createAnimation()
|
|
98
|
+
.delay(ANIMATION_DELAY_BASE)
|
|
99
|
+
.duration(ANIMATION_DURATION)
|
|
100
|
+
.easing(ANIMATION_EASING)
|
|
101
|
+
.addElement(ionFooter)
|
|
102
|
+
.afterAddWrite(() => (ionFooter.style.pointerEvents = 'auto'))
|
|
103
|
+
.fromTo('opacity', '0.8', '1')
|
|
104
|
+
.addAnimation([tabBarAnimation, fabButtonAnimation, searchContainerAnimation, effectAnimation, closeButtonsAnimation])
|
|
105
|
+
.play();
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
elementSizes: sizes,
|
|
109
|
+
colorSelected,
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
const leaveEvent = async (
|
|
114
|
+
event: Event,
|
|
115
|
+
searchableEventCache: SearchableEventCache,
|
|
116
|
+
ionTabBar: HTMLElement,
|
|
117
|
+
ionFabButton: HTMLElement,
|
|
118
|
+
ionFooter: HTMLElement,
|
|
119
|
+
): Promise<void> => {
|
|
120
|
+
if (!(event.target as HTMLElement)?.closest('ion-buttons[slot=start] ion-button')) {
|
|
121
|
+
throw throwErrorByFailedClickElement('ion-buttons[slot=start] ion-button');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const references = getElementReferences(ionTabBar, ionFooter);
|
|
125
|
+
|
|
126
|
+
const effectAnimation = createReverseEffectAnimation(references, searchableEventCache.elementSizes, searchableEventCache.colorSelected);
|
|
127
|
+
const searchContainerAnimation = createReverseSearchContainerAnimation(references, searchableEventCache.elementSizes);
|
|
128
|
+
const closeButtonsAnimation = createReverseCloseButtonsAnimation(references);
|
|
129
|
+
const tabBarAnimation = createReverseTabBarAnimation(ionTabBar, references, searchableEventCache.elementSizes);
|
|
130
|
+
const fabButtonAnimation = createReverseFabButtonAnimation(ionFabButton, searchableEventCache.elementSizes);
|
|
131
|
+
|
|
132
|
+
await createAnimation()
|
|
133
|
+
.delay(ANIMATION_DELAY_BASE)
|
|
134
|
+
.duration(ANIMATION_DURATION)
|
|
135
|
+
.easing(ANIMATION_EASING)
|
|
136
|
+
.addElement(ionFooter)
|
|
137
|
+
.afterAddWrite(() => (ionFooter.style.pointerEvents = 'none'))
|
|
138
|
+
.fromTo('opacity', '1', '0')
|
|
139
|
+
.addAnimation([tabBarAnimation, fabButtonAnimation, searchContainerAnimation, effectAnimation, closeButtonsAnimation])
|
|
140
|
+
.play();
|
|
141
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export enum TabBarSearchableType {
|
|
2
|
+
Enter = 'enter',
|
|
3
|
+
Leave = 'leave',
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export type TabBarSearchableFunction = (event: Event, type: TabBarSearchableType) => Promise<void>;
|
|
7
|
+
|
|
8
|
+
export interface SearchableEventCache {
|
|
9
|
+
elementSizes: ElementSizes;
|
|
10
|
+
colorSelected: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// DOM要素とサイズ情報の型定義
|
|
14
|
+
export interface ElementSizes {
|
|
15
|
+
tabBar: { width: number; height: number };
|
|
16
|
+
closeButton: { width: number; height: number };
|
|
17
|
+
fabButton: { width: number; height: number };
|
|
18
|
+
searchContainer: { width: number; height: number };
|
|
19
|
+
selectedTabButtonIcon: { width: number; height: number; top: number; left: number };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ElementReferences {
|
|
23
|
+
searchContainer: HTMLElement;
|
|
24
|
+
closeButtons: HTMLElement;
|
|
25
|
+
selectedTabButton: HTMLElement;
|
|
26
|
+
selectedTabButtonIcon: HTMLElement;
|
|
27
|
+
closeButtonIcon: HTMLElement;
|
|
28
|
+
}
|