@rdlabo/ionic-theme-ios27 1.1.0 → 1.2.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.
Files changed (93) hide show
  1. package/README.md +8 -1
  2. package/dist/css/components/ion-list.css +1 -1
  3. package/dist/css/components/ion-menu.css +1 -1
  4. package/dist/css/ionic-theme-ios27.css +1 -1
  5. package/dist/css/vertical-bars.css +1 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +15 -8
  8. package/dist/native/components/index.d.ts +1 -0
  9. package/dist/native/components/index.d.ts.map +1 -1
  10. package/dist/native/components/index.js +8 -3
  11. package/dist/native/components/ion-back-button.d.ts.map +1 -1
  12. package/dist/native/components/ion-back-button.js +5 -2
  13. package/dist/native/components/ion-button.d.ts.map +1 -1
  14. package/dist/native/components/ion-button.js +10 -2
  15. package/dist/native/components/ion-buttons.d.ts.map +1 -1
  16. package/dist/native/components/ion-buttons.js +12 -4
  17. package/dist/native/components/ion-fab.js +4 -4
  18. package/dist/native/components/ion-menu-button.d.ts.map +1 -1
  19. package/dist/native/components/ion-menu-button.js +5 -4
  20. package/dist/native/components/searchable-tabs.d.ts.map +1 -1
  21. package/dist/native/components/searchable-tabs.js +4 -1
  22. package/dist/native/definitions.d.ts +29 -3
  23. package/dist/native/definitions.d.ts.map +1 -1
  24. package/dist/native/definitions.js +6 -1
  25. package/dist/native/index.d.ts +6 -2
  26. package/dist/native/index.d.ts.map +1 -1
  27. package/dist/native/index.js +154 -17
  28. package/dist/native/prehide.d.ts +5 -0
  29. package/dist/native/prehide.d.ts.map +1 -0
  30. package/dist/native/prehide.js +246 -0
  31. package/dist/native/runtime.d.ts +1 -1
  32. package/dist/native/runtime.d.ts.map +1 -1
  33. package/dist/native/runtime.js +162 -28
  34. package/dist/native/shared/candidate.d.ts.map +1 -1
  35. package/dist/native/shared/candidate.js +7 -3
  36. package/dist/native/shared/dom.d.ts +33 -2
  37. package/dist/native/shared/dom.d.ts.map +1 -1
  38. package/dist/native/shared/dom.js +171 -10
  39. package/dist/native/vertical-bars-web.d.ts +3 -0
  40. package/dist/native/vertical-bars-web.d.ts.map +1 -0
  41. package/dist/native/vertical-bars-web.js +341 -0
  42. package/dist/native-integration/index.d.ts +3 -2
  43. package/dist/native-integration/index.d.ts.map +1 -1
  44. package/dist/native-integration/index.js +11 -5
  45. package/dist/transition/index.d.ts +2 -2
  46. package/dist/transition/index.d.ts.map +1 -1
  47. package/dist/transition/index.js +1 -1
  48. package/dist/transition/ios.transition.d.ts.map +1 -1
  49. package/dist/transition/ios.transition.js +1 -0
  50. package/dist/utils.d.ts +3 -3
  51. package/dist/utils.d.ts.map +1 -1
  52. package/dist/utils.js +1 -1
  53. package/dist/vertical-bars.d.ts +4 -0
  54. package/dist/vertical-bars.d.ts.map +1 -0
  55. package/dist/vertical-bars.js +2 -0
  56. package/docs/iphone-duo.md +255 -0
  57. package/docs/migration.md +2 -2
  58. package/docs/native-ui-shell.md +15 -3
  59. package/docs/special-markup.md +6 -0
  60. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellSearchController.swift +15 -1
  61. package/ios/Sources/IonicNativeUIShellPlugin/Components/ShellVerticalBars.swift +358 -0
  62. package/ios/Sources/IonicNativeUIShellPlugin/IonicNativeUIShellPlugin.swift +215 -35
  63. package/ios/Sources/IonicNativeUIShellPlugin/Shared/ShellSnapshot.swift +10 -1
  64. package/ios/Tests/IonicNativeUIShellPluginTests/ShellSnapshotTests.swift +29 -0
  65. package/package.json +6 -2
  66. package/src/index.ts +15 -8
  67. package/src/native/components/index.ts +12 -3
  68. package/src/native/components/ion-back-button.ts +8 -2
  69. package/src/native/components/ion-button.ts +12 -2
  70. package/src/native/components/ion-buttons.ts +9 -4
  71. package/src/native/components/ion-fab.ts +4 -4
  72. package/src/native/components/ion-menu-button.ts +5 -4
  73. package/src/native/components/searchable-tabs.ts +9 -1
  74. package/src/native/definitions.ts +40 -3
  75. package/src/native/index.ts +188 -20
  76. package/src/native/prehide.ts +255 -0
  77. package/src/native/runtime.ts +173 -27
  78. package/src/native/shared/candidate.ts +11 -3
  79. package/src/native/shared/dom.ts +212 -11
  80. package/src/native/vertical-bars-web.ts +374 -0
  81. package/src/native-integration/index.ts +13 -8
  82. package/src/styles/components/ion-list.scss +2 -2
  83. package/src/styles/components/ion-menu.scss +2 -2
  84. package/src/styles/ionic-theme-ios27.scss +1 -0
  85. package/src/styles/vertical-bars.scss +342 -0
  86. package/src/transition/index.ts +11 -6
  87. package/src/transition/ios.transition.ts +1 -0
  88. package/src/utils.ts +8 -8
  89. package/src/vertical-bars.ts +3 -0
  90. package/dist/native/lifecycle.d.ts +0 -4
  91. package/dist/native/lifecycle.d.ts.map +0 -1
  92. package/dist/native/lifecycle.js +0 -34
  93. package/src/native/lifecycle.ts +0 -38
@@ -1,4 +1,4 @@
1
- import { frame, isDark, text, visible } from './dom';
1
+ import { frame, isDark, isVerticalBarsSource, text, visible } from './dom';
2
2
  import { iconSource } from './icons';
3
3
  export const createCandidate = (element, kind, id) => {
4
4
  const rect = element.getBoundingClientRect();
@@ -18,7 +18,8 @@ export const createCandidate = (element, kind, id) => {
18
18
  };
19
19
  };
20
20
  export const appendItem = (candidate, child, id, content = child, label = text(child)) => {
21
- if (!visible(child) || child.querySelector('input, button, a, img, canvas, video, ion-spinner, ion-avatar'))
21
+ if (!visible(child, isVerticalBarsSource(candidate.element)) ||
22
+ child.querySelector('input, button, a, img, canvas, video, ion-spinner, ion-avatar'))
22
23
  return;
23
24
  const icons = Array.from(content.querySelectorAll('ion-icon'));
24
25
  const directSVGs = Array.from(content.querySelectorAll('svg'));
@@ -36,7 +37,10 @@ export const appendItem = (candidate, child, id, content = child, label = text(c
36
37
  id: id(child),
37
38
  ...frame(child.getBoundingClientRect(), candidate.element.getBoundingClientRect()),
38
39
  label,
39
- accessibilityLabel: child.getAttribute('aria-label') ?? native?.getAttribute('aria-label') ?? label,
40
+ accessibilityLabel: child.getAttribute('aria-label') ??
41
+ native?.getAttribute('aria-label') ??
42
+ (icons[0]?.getAttribute('aria-hidden') === 'true' ? null : icons[0]?.getAttribute('aria-label')) ??
43
+ label,
40
44
  disabled: !!child.disabled ||
41
45
  !!candidate.element.disabled ||
42
46
  getComputedStyle(child).pointerEvents === 'none',
@@ -1,10 +1,41 @@
1
1
  import type { Frame } from '../definitions';
2
2
  export declare const marker = "data-native-ui-shell";
3
+ export declare const childNodesOf: (element: Element) => Node[];
4
+ export declare const childElements: (element: Element) => HTMLElement[];
5
+ export declare const prehiddenClass = "ios-theme-native-ui-shell-prehidden";
6
+ export declare const prehideRootClass = "ios-theme-native-ui-shell-prehide";
7
+ export declare const rejectedClass = "ios-theme-native-ui-shell-rejected";
8
+ export declare const verticalBarsBackWebClass = "ios-theme-vertical-bars-back-web-owned";
9
+ export declare const prehideOnlyMutation: (record: MutationRecord) => boolean;
10
+ export declare const withoutPrehide: <T>(element: HTMLElement, read: () => T) => T;
3
11
  export declare const isDark: (style: CSSStyleDeclaration) => boolean;
4
- export declare const excluded = ".ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-page-hidden, .ion-page-invisible, .ion-cloned-element, [hidden], [inert]";
12
+ export declare const excluded = ".ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-cloned-element, [hidden], [inert], .ion-page-hidden, .ion-page-invisible";
13
+ export declare const setVerticalBarsEnteringPage: (page: HTMLElement, entering: boolean) => void;
14
+ export declare const verticalBarsEnteringPage: (element: HTMLElement) => HTMLElement | undefined;
15
+ export declare const createVerticalBarsPageState: () => {
16
+ isDeparted(element: HTMLElement): boolean;
17
+ lifecycle(event: Event): void;
18
+ cancel(entering?: HTMLElement, leaving?: HTMLElement): void;
19
+ };
20
+ export declare const isDisabledButtonGroupChild: (element: HTMLElement) => boolean;
21
+ export declare const isVerticalBarsSource: (element: HTMLElement) => boolean;
22
+ export declare const isVerticalBarsBackPosition: (element: HTMLElement) => boolean;
23
+ export declare const preferredVerticalBarsBack: <T extends HTMLElement>(elements: T[], doc: Document) => T | undefined;
24
+ export declare const isPermanentlyExcluded: (element: HTMLElement) => boolean;
25
+ export declare const isExcluded: (element: HTMLElement, enteringPage?: HTMLElement) => boolean;
5
26
  export declare const isShellDisabled: (element: Element) => boolean;
27
+ export declare const isVerticalBarsToolbarActionShape: (element: HTMLElement) => boolean;
28
+ export declare const setVerticalBarsPlacement: (element: HTMLElement, rail: boolean) => void;
29
+ export declare const clearVerticalBarsPlacement: (element: HTMLElement) => void;
30
+ export declare const verticalBarsOwned: (element: HTMLElement) => boolean;
31
+ export declare const isVerticalBarsToolbarAction: (element: HTMLElement) => boolean;
32
+ export declare const verticalBarsActionCandidate: (element: HTMLElement) => boolean;
33
+ export declare const verticalBarsBackCandidate: (element: HTMLElement) => boolean;
34
+ export declare const verticalBarsToolbarActions: (element: HTMLElement) => HTMLElement[];
35
+ export declare const isVerticalBarsToolbarGroup: (element: HTMLElement) => boolean;
36
+ export declare const activateProjectedElement: (element: HTMLElement) => void;
6
37
  export declare const unprojected: <T>(elements: Iterable<HTMLElement>, read: () => T) => T;
7
- export declare const visible: (element: HTMLElement) => boolean;
38
+ export declare const visible: (element: HTMLElement, allowOutsideViewport?: boolean) => boolean;
8
39
  export declare const frame: (rect: DOMRect, origin?: DOMRect) => Frame;
9
40
  export declare const text: (element: Element) => string;
10
41
  export declare const inFixedToolbar: (element: Element) => boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../../src/native/shared/dom.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,gBAAgB,CAAC;AAE5C,eAAO,MAAM,MAAM,yBAAyB,CAAC;AAC7C,eAAO,MAAM,MAAM,GAAI,OAAO,mBAAmB,KAAG,OAA2E,CAAC;AAChI,eAAO,MAAM,QAAQ,+IACyH,CAAC;AAI/I,eAAO,MAAM,eAAe,GAAI,SAAS,OAAO,KAAG,OACyC,CAAC;AAE7F,eAAO,MAAM,WAAW,GAAI,CAAC,EAAE,UAAU,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,MAAM,CAAC,KAAG,CAQ/E,CAAC;AAEF,eAAO,MAAM,OAAO,GAAI,SAAS,WAAW,KAAG,OAmB9C,CAAC;AAEF,eAAO,MAAM,KAAK,GAAI,MAAM,OAAO,EAAE,SAAS,OAAO,KAAG,KAKtD,CAAC;AAEH,eAAO,MAAM,IAAI,GAAI,SAAS,OAAO,KAAG,MAOvC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,OAAO,KAAG,OAQjD,CAAC"}
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../../src/native/shared/dom.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,162 @@
1
1
  import { fadeMarker } from './crossfade';
2
2
  export const marker = 'data-native-ui-shell';
3
- export const isDark = (style) => style.getPropertyValue('--ios27-color-scheme').trim() === 'dark';
4
- export const excluded = '.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-page-hidden, .ion-page-invisible, .ion-cloned-element, [hidden], [inert]';
3
+ const childNodesGet = typeof Node === 'undefined' ? undefined : Object.getOwnPropertyDescriptor(Node.prototype, 'childNodes')?.get;
4
+ export const childNodesOf = (element) => childNodesGet ? Array.from(childNodesGet.call(element)) : Array.from(element.childNodes);
5
+ export const childElements = (element) => childNodesOf(element).filter((node) => node.nodeType === 1);
6
+ export const prehiddenClass = 'ios-theme-native-ui-shell-prehidden';
7
+ export const prehideRootClass = 'ios-theme-native-ui-shell-prehide';
8
+ export const rejectedClass = 'ios-theme-native-ui-shell-rejected';
9
+ export const verticalBarsBackWebClass = 'ios-theme-vertical-bars-back-web-owned';
10
+ const prehideClasses = new Set([prehiddenClass, prehideRootClass]);
11
+ export const prehideOnlyMutation = (record) => {
12
+ if (record.attributeName !== 'class' || record.oldValue === null)
13
+ return false;
14
+ const withoutPrehide = (value) => value
15
+ .split(/\s+/)
16
+ .filter((name) => name && !prehideClasses.has(name))
17
+ .join(' ');
18
+ return withoutPrehide(record.oldValue) === withoutPrehide(record.target.getAttribute('class') ?? '');
19
+ };
20
+ export const withoutPrehide = (element, read) => {
21
+ const changed = [];
22
+ for (let current = element; current; current = current.parentElement) {
23
+ if (current.classList.contains(prehiddenClass)) {
24
+ current.classList.remove(prehiddenClass);
25
+ changed.push(current);
26
+ }
27
+ }
28
+ const root = element.ownerDocument.documentElement;
29
+ if (element.matches('ion-back-button') && root.classList.contains(prehideRootClass)) {
30
+ root.classList.remove(prehideRootClass);
31
+ changed.push(root);
32
+ }
33
+ try {
34
+ return read();
35
+ }
36
+ finally {
37
+ changed.forEach((current) => current.classList.add(current === root ? prehideRootClass : prehiddenClass));
38
+ }
39
+ };
40
+ export const isDark = (style) => {
41
+ const themeScheme = style.getPropertyValue('--ios27-color-scheme').trim();
42
+ if (themeScheme)
43
+ return themeScheme === 'dark';
44
+ const background = style.getPropertyValue('--ion-background-color-rgb').match(/\d+/g)?.slice(0, 3).map(Number);
45
+ return !!background && background.length === 3 && background[0] * 0.2126 + background[1] * 0.7152 + background[2] * 0.0722 < 128;
46
+ };
47
+ const permanentlyExcluded = '.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled, .ion-cloned-element, [hidden], [inert]';
48
+ export const excluded = `${permanentlyExcluded}, .ion-page-hidden, .ion-page-invisible`;
49
+ const enteringPages = new WeakSet();
50
+ export const setVerticalBarsEnteringPage = (page, entering) => {
51
+ if (entering)
52
+ enteringPages.add(page);
53
+ else
54
+ enteringPages.delete(page);
55
+ };
56
+ export const verticalBarsEnteringPage = (element) => {
57
+ const page = element.closest('.ion-page-invisible');
58
+ return page && enteringPages.has(page) && page.closest('ion-app.ios-theme-vertical-bars') ? page : undefined;
59
+ };
60
+ export const createVerticalBarsPageState = () => {
61
+ const departed = new WeakSet();
62
+ return {
63
+ isDeparted(element) {
64
+ const page = element.closest('.ion-page');
65
+ return !!page && departed.has(page) && !!element.closest('ion-app.ios-theme-vertical-bars');
66
+ },
67
+ lifecycle(event) {
68
+ const page = event.target && event.target.nodeType === 1 ? event.target : undefined;
69
+ if (!page?.matches('.ion-page'))
70
+ return;
71
+ const entering = event.type === 'ionViewWillEnter';
72
+ if (entering || event.type === 'ionViewDidEnter')
73
+ departed.delete(page);
74
+ else if (event.type === 'ionViewWillLeave' || event.type === 'ionViewDidLeave')
75
+ departed.add(page);
76
+ setVerticalBarsEnteringPage(page, entering);
77
+ },
78
+ cancel(entering, leaving) {
79
+ if (entering) {
80
+ departed.add(entering);
81
+ setVerticalBarsEnteringPage(entering, false);
82
+ }
83
+ if (leaving)
84
+ departed.delete(leaving);
85
+ },
86
+ };
87
+ };
88
+ const disabledButtonGroup = 'ion-buttons:is(.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled)';
5
89
  const shellDisabledSelector = '.ios-theme-shell-disabled';
90
+ export const isDisabledButtonGroupChild = (element) => element.matches('ion-button') && element.parentElement?.matches(disabledButtonGroup) === true;
91
+ const verticalBarsTags = new Set(['ion-button', 'ion-back-button', 'ion-buttons', 'ion-menu-button', 'ion-tab-bar']);
92
+ export const isVerticalBarsSource = (element) => verticalBarsTags.has(element.localName) &&
93
+ (element.matches('ion-tab-bar') || verticalBarsOwned(element)) &&
94
+ !element.closest('ion-menu, ion-modal, ion-popover') &&
95
+ !!element.closest('ion-app.ios-theme-vertical-bars');
96
+ export const isVerticalBarsBackPosition = (element) => {
97
+ if (element.closest('ion-header[collapse], ion-footer[collapse]'))
98
+ return false;
99
+ const page = element.closest('.ion-page');
100
+ if (!page)
101
+ return true;
102
+ const candidates = Array.from(page.querySelectorAll('ion-back-button')).filter((back) => back.closest('.ion-page') === page &&
103
+ !back.matches('.ion-cloned-element') &&
104
+ !back.closest('ion-header[collapse], ion-footer[collapse], ion-menu, ion-modal, ion-popover'));
105
+ const rank = (back) => inFixedToolbar(back) ? 0 : back.closest('ion-header ion-toolbar, ion-footer ion-toolbar') ? 1 : back.closest('ion-toolbar') ? 3 : 2;
106
+ const best = candidates.reduce((winner, back) => (!winner || rank(back) < rank(winner) ? back : winner), undefined);
107
+ return best === element;
108
+ };
109
+ export const preferredVerticalBarsBack = (elements, doc) => {
110
+ const pages = Array.from(doc.querySelectorAll('.ion-page'));
111
+ const pageOrder = (element) => pages.indexOf(element.closest('.ion-page'));
112
+ return elements.sort((a, b) => pageOrder(b) - pageOrder(a) || Number(!!b.closest('ion-header')) - Number(!!a.closest('ion-header')))[0];
113
+ };
114
+ const excludedBy = (element, selector) => {
115
+ const owner = element.closest(selector);
116
+ return !!owner && !(element.parentElement === owner && isDisabledButtonGroupChild(element));
117
+ };
118
+ export const isPermanentlyExcluded = (element) => excludedBy(element, permanentlyExcluded);
119
+ export const isExcluded = (element, enteringPage) => excludedBy(element, `${permanentlyExcluded}, .ion-page-hidden`) || (!!element.closest('.ion-page-invisible') && !enteringPage);
6
120
  export const isShellDisabled = (element) => !!element.closest(shellDisabledSelector) || !!element.querySelector(shellDisabledSelector);
121
+ export const isVerticalBarsToolbarActionShape = (element) => element.matches('ion-menu-button') ||
122
+ (element.matches('ion-button') &&
123
+ !!element.querySelector('ion-icon, svg') &&
124
+ !childNodesOf(element).some((node) => node.nodeType === 3 && !!node.textContent?.trim()) &&
125
+ !element.matches('.ion-color, [color]') &&
126
+ ['default', 'clear'].includes(element.fill ?? element.getAttribute('fill') ?? 'default'));
127
+ const verticalBarsPlacement = new WeakMap();
128
+ export const setVerticalBarsPlacement = (element, rail) => {
129
+ verticalBarsPlacement.set(element, rail);
130
+ };
131
+ export const clearVerticalBarsPlacement = (element) => {
132
+ verticalBarsPlacement.delete(element);
133
+ };
134
+ export const verticalBarsOwned = (element) => verticalBarsPlacement.get(element) === true;
135
+ export const isVerticalBarsToolbarAction = (element) => verticalBarsOwned(element) && !isExcluded(element, verticalBarsEnteringPage(element)) && !isShellDisabled(element);
136
+ const overlays = 'ion-menu, ion-modal, ion-popover';
137
+ export const verticalBarsActionCandidate = (element) => inFixedToolbar(element) &&
138
+ !element.closest('ion-buttons.ios-theme-horizontal-only, ion-button.ios-theme-horizontal-only') &&
139
+ !isPermanentlyExcluded(element) &&
140
+ !isShellDisabled(element) &&
141
+ !element.closest(overlays);
142
+ export const verticalBarsBackCandidate = (element) => isVerticalBarsBackPosition(element) &&
143
+ !element.closest('ion-buttons.ios-theme-horizontal-only') &&
144
+ !isPermanentlyExcluded(element) &&
145
+ !isShellDisabled(element) &&
146
+ !element.closest(overlays);
147
+ export const verticalBarsToolbarActions = (element) => childElements(element).filter((child) => !child.matches('ion-back-button') && isVerticalBarsToolbarAction(child));
148
+ export const isVerticalBarsToolbarGroup = (element) => {
149
+ return (element.matches('ion-buttons') &&
150
+ verticalBarsOwned(element) &&
151
+ !element.matches('.ionic-theme-disabled, .ios-theme-disabled, .ios26-disabled') &&
152
+ !isShellDisabled(element));
153
+ };
154
+ export const activateProjectedElement = (element) => {
155
+ const target = element.matches('ion-button, ion-back-button, ion-menu-button')
156
+ ? element.shadowRoot?.querySelector('[part~="native"]')
157
+ : undefined;
158
+ (target ?? element).click();
159
+ };
7
160
  export const unprojected = (elements, read) => {
8
161
  const hidden = Array.from(elements).filter((element) => element.hasAttribute(marker));
9
162
  hidden.forEach((element) => element.removeAttribute(marker));
@@ -14,22 +167,29 @@ export const unprojected = (elements, read) => {
14
167
  hidden.forEach((element) => element.setAttribute(marker, ''));
15
168
  }
16
169
  };
17
- export const visible = (element) => {
18
- if (!element.isConnected || element.closest(excluded) || isShellDisabled(element))
170
+ const readVisible = (element, allowOutsideViewport) => {
171
+ const enteringPage = isVerticalBarsSource(element) ? verticalBarsEnteringPage(element) : undefined;
172
+ if (!element.isConnected || isExcluded(element, enteringPage) || isShellDisabled(element))
19
173
  return false;
20
174
  for (let current = element; current; current = current.parentElement) {
21
175
  const style = getComputedStyle(current);
22
- if (style.display === 'none' || style.visibility !== 'visible' || (Number(style.opacity) === 0 && !current.hasAttribute(fadeMarker)))
176
+ if (style.display === 'none' ||
177
+ style.visibility !== 'visible' ||
178
+ (Number(style.opacity) === 0 && !current.hasAttribute(fadeMarker) && current !== enteringPage))
23
179
  return false;
24
- if (style.transform !== 'none' &&
180
+ if (!allowOutsideViewport &&
181
+ style.transform !== 'none' &&
25
182
  !new DOMMatrixReadOnly(style.transform).isIdentity &&
26
183
  current !== element &&
27
184
  !current.matches('ion-tab-bar'))
28
185
  return false;
29
186
  }
30
187
  const rect = element.getBoundingClientRect();
31
- return (rect.width > 0 && rect.height > 0 && rect.left >= -1 && rect.top >= -1 && rect.right <= innerWidth + 1 && rect.bottom <= innerHeight + 1);
188
+ return (rect.width > 0 &&
189
+ rect.height > 0 &&
190
+ (allowOutsideViewport || (rect.left >= -1 && rect.top >= -1 && rect.right <= innerWidth + 1 && rect.bottom <= innerHeight + 1)));
32
191
  };
192
+ export const visible = (element, allowOutsideViewport = false) => withoutPrehide(element, () => readVisible(element, allowOutsideViewport));
33
193
  export const frame = (rect, origin) => ({
34
194
  x: rect.x - (origin?.x ?? 0),
35
195
  y: rect.y - (origin?.y ?? 0),
@@ -39,16 +199,17 @@ export const frame = (rect, origin) => ({
39
199
  export const text = (element) => {
40
200
  if (element.matches('ion-icon, svg, ion-badge, .ios27-segment-lens, .ion-cloned-element'))
41
201
  return '';
42
- return Array.from(element.childNodes)
43
- .map((node) => (node.nodeType === Node.TEXT_NODE ? node.textContent : node instanceof Element ? text(node) : ''))
202
+ return childNodesOf(element)
203
+ .map((node) => (node.nodeType === 3 ? node.textContent : node.nodeType === 1 ? text(node) : ''))
44
204
  .join('')
45
205
  .replace(/\s+/g, ' ')
46
206
  .trim();
47
207
  };
48
208
  export const inFixedToolbar = (element) => {
49
209
  const edge = element.closest('ion-toolbar')?.parentElement;
210
+ const verticalBars = !!element.closest('ion-app.ios-theme-vertical-bars');
50
211
  return (!!edge?.matches('ion-header, ion-footer') &&
51
212
  !element.closest('ion-content') &&
52
213
  !edge.hasAttribute('collapse') &&
53
- !edge.matches('.header-collapse-main, .header-collapse-condense'));
214
+ (verticalBars || !edge.matches('.header-collapse-main, .header-collapse-condense')));
54
215
  };
@@ -0,0 +1,3 @@
1
+ import type { NativeUIShellHandle, NativeUIShellOptions } from './definitions';
2
+ export declare const createVerticalBarsWebProjection: (doc: Document, options: NativeUIShellOptions, enabled?: () => boolean) => NativeUIShellHandle;
3
+ //# sourceMappingURL=vertical-bars-web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vertical-bars-web.d.ts","sourceRoot":"","sources":["../../src/native/vertical-bars-web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAqCpG,eAAO,MAAM,+BAA+B,GAC1C,KAAK,QAAQ,EACb,SAAS,oBAAoB,EAC7B,UAAS,MAAM,OAAoB,KAClC,mBA4UF,CAAC"}
@@ -0,0 +1,341 @@
1
+ import { VERTICAL_BARS_TRANSITION_CANCELED } from '../native-integration';
2
+ import { activateProjectedElement, createVerticalBarsPageState, verticalBarsActionCandidate, verticalBarsBackCandidate, verticalBarsEnteringPage, verticalBarsToolbarActions, isExcluded, isVerticalBarsToolbarGroup, preferredVerticalBarsBack, verticalBarsOwned, marker, prehideOnlyMutation, prehiddenClass, unprojected, withoutPrehide, } from './shared/dom';
3
+ const backProjectionClass = 'ios-theme-vertical-bars-back-button-projection';
4
+ const toolbarProjectionClass = 'ios-theme-vertical-bars-toolbar-projection';
5
+ const readyClass = 'ios-theme-vertical-bars-toolbar-ready';
6
+ const toolbarControlSize = 46;
7
+ const toolbarControlGap = 10;
8
+ export const createVerticalBarsWebProjection = (doc, options, enabled = () => true) => {
9
+ const win = doc.defaultView;
10
+ if (options.controls !== undefined && options.controls.toolbar !== true)
11
+ return {
12
+ getStatus: () => ({ state: 'web', projected: 0, updates: 0 }),
13
+ suspend: async () => ({ resume: async () => { } }),
14
+ destroy: async () => { },
15
+ };
16
+ let root;
17
+ let backSource;
18
+ let backProjection;
19
+ let previousBackAriaHidden = null;
20
+ let toolbarProjections = [];
21
+ let suspended = 0;
22
+ let stopped = false;
23
+ let frame = 0;
24
+ let updates = 0;
25
+ let observingVerticalBars = false;
26
+ const verticalBarsPages = createVerticalBarsPageState();
27
+ let sourceObserver;
28
+ let waiters = [];
29
+ const listeners = new AbortController();
30
+ const verticalBarsRoot = () => doc.querySelector('ion-app.ios-theme-vertical-bars');
31
+ const projectedSources = () => [backSource, ...toolbarProjections.flatMap(({ actions }) => actions.map(({ source }) => source))].filter((source) => !!source);
32
+ const isRendered = (element) => withoutPrehide(element, () => {
33
+ const style = getComputedStyle(element);
34
+ const rect = element.getBoundingClientRect();
35
+ return element.isConnected && style.display !== 'none' && style.visibility === 'visible' && rect.width > 0 && rect.height > 0;
36
+ });
37
+ const inEligibleToolbar = (element) => !!verticalBarsRoot()?.contains(element) &&
38
+ verticalBarsActionCandidate(element) &&
39
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
40
+ !verticalBarsPages.isDeparted(element);
41
+ const isEligibleBack = (element) => !!verticalBarsRoot()?.contains(element) &&
42
+ verticalBarsOwned(element) &&
43
+ verticalBarsBackCandidate(element) &&
44
+ !isExcluded(element, verticalBarsEnteringPage(element)) &&
45
+ !verticalBarsPages.isDeparted(element) &&
46
+ unprojected(projectedSources(), () => isRendered(element));
47
+ const pageOrder = (element) => Array.from(doc.querySelectorAll('.ion-page')).indexOf(element.closest('.ion-page'));
48
+ const findBack = () => {
49
+ const candidates = Array.from(doc.querySelectorAll(`ion-back-button:not(.${backProjectionClass})`)).filter(isEligibleBack);
50
+ return preferredVerticalBarsBack(candidates, doc);
51
+ };
52
+ const findToolbarGroups = () => {
53
+ const candidates = Array.from(doc.querySelectorAll(`ion-buttons:not(.${toolbarProjectionClass})`)).flatMap((group) => {
54
+ const actions = verticalBarsToolbarActions(group).filter((action) => unprojected(projectedSources(), () => isRendered(action)));
55
+ if (!actions.length)
56
+ return [];
57
+ if (!isVerticalBarsToolbarGroup(group))
58
+ return actions.filter(inEligibleToolbar).map((action) => ({ group, actions: [action] }));
59
+ return inEligibleToolbar(group) && unprojected(projectedSources(), () => isRendered(group)) ? [{ group, actions }] : [];
60
+ });
61
+ const highestPage = Math.max(...candidates.map(({ group }) => pageOrder(group)));
62
+ return candidates.filter(({ group }) => pageOrder(group) === highestPage);
63
+ };
64
+ const isCurrentToolbarAction = (source) => findToolbarGroups().some(({ actions }) => actions.includes(source));
65
+ const restore = () => {
66
+ backProjection?.remove();
67
+ backProjection = undefined;
68
+ if (backSource) {
69
+ backSource.removeAttribute(marker);
70
+ if (previousBackAriaHidden === null)
71
+ backSource.removeAttribute('aria-hidden');
72
+ else
73
+ backSource.setAttribute('aria-hidden', previousBackAriaHidden);
74
+ backSource.dispatchEvent(new CustomEvent('nativeUIShellChange'));
75
+ }
76
+ backSource = undefined;
77
+ previousBackAriaHidden = null;
78
+ for (const { projection, actions } of toolbarProjections) {
79
+ projection.remove();
80
+ for (const { source, previousAriaHidden } of actions) {
81
+ source.removeAttribute(marker);
82
+ if (previousAriaHidden === null)
83
+ source.removeAttribute('aria-hidden');
84
+ else
85
+ source.setAttribute('aria-hidden', previousAriaHidden);
86
+ source.dispatchEvent(new CustomEvent('nativeUIShellChange'));
87
+ }
88
+ }
89
+ toolbarProjections = [];
90
+ sourceObserver?.disconnect();
91
+ sourceObserver = undefined;
92
+ root?.classList.remove(readyClass);
93
+ root = undefined;
94
+ };
95
+ const copyAttributes = (target, original) => {
96
+ for (const name of ['id', 'slot', marker, 'aria-hidden'])
97
+ target.removeAttribute(name);
98
+ for (const attribute of Array.from(target.attributes))
99
+ if (!['class', marker].includes(attribute.name) && !original.hasAttribute(attribute.name))
100
+ target.removeAttribute(attribute.name);
101
+ for (const attribute of Array.from(original.attributes))
102
+ if (!['id', 'slot', marker, 'aria-hidden'].includes(attribute.name))
103
+ target.setAttribute(attribute.name, attribute.value);
104
+ target.classList.remove(prehiddenClass);
105
+ };
106
+ const syncBack = (target, original) => {
107
+ copyAttributes(target, original);
108
+ target.classList.add(backProjectionClass, 'ion-cloned-element');
109
+ target.disabled = original.disabled;
110
+ target.defaultHref = original.defaultHref;
111
+ target.icon = original.icon;
112
+ target.color = original.color;
113
+ target.mode = original.mode;
114
+ target.text = '';
115
+ };
116
+ const syncAction = (target, original) => {
117
+ copyAttributes(target, original);
118
+ target.classList.add('ios-theme-vertical-bars-toolbar-action', 'ion-cloned-element');
119
+ const label = original.getAttribute('aria-label') ?? original.textContent?.trim();
120
+ if (label)
121
+ target.setAttribute('aria-label', label);
122
+ if ('disabled' in original)
123
+ target.disabled = original.disabled;
124
+ if (original.matches('ion-button')) {
125
+ const icon = original.querySelector('ion-icon, svg');
126
+ const clonedIcon = icon?.cloneNode(true);
127
+ clonedIcon?.setAttribute('slot', 'icon-only');
128
+ target.replaceChildren(...(clonedIcon ? [clonedIcon] : []));
129
+ }
130
+ };
131
+ const sameSources = (back, groups) => back === backSource &&
132
+ groups.length === toolbarProjections.length &&
133
+ groups.every(({ group, actions }, index) => {
134
+ const current = toolbarProjections[index];
135
+ return (group === current.source &&
136
+ actions.length === current.actions.length &&
137
+ actions.every((action, actionIndex) => action === current.actions[actionIndex].source));
138
+ });
139
+ const syncExisting = () => {
140
+ if (backSource && backProjection)
141
+ syncBack(backProjection, backSource);
142
+ let topOffset = backSource ? toolbarControlSize + toolbarControlGap : 0;
143
+ for (const { projection, source, actions } of toolbarProjections) {
144
+ if (actions.length === 1 && !projection.matches('ion-buttons'))
145
+ syncAction(projection, actions[0].source);
146
+ else
147
+ copyAttributes(projection, source);
148
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
149
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
150
+ if (projection.matches('ion-buttons'))
151
+ actions.forEach(({ source: action, projection: clone }) => syncAction(clone, action));
152
+ topOffset += actions.length * toolbarControlSize + toolbarControlGap;
153
+ }
154
+ };
155
+ const project = (nextBack, groups) => {
156
+ root = verticalBarsRoot();
157
+ let topOffset = 0;
158
+ if (nextBack) {
159
+ backSource = nextBack;
160
+ previousBackAriaHidden = nextBack.getAttribute('aria-hidden');
161
+ backProjection = nextBack.cloneNode(false);
162
+ syncBack(backProjection, nextBack);
163
+ backProjection.addEventListener('click', (event) => {
164
+ event.preventDefault();
165
+ event.stopImmediatePropagation();
166
+ if (backSource && backSource === findBack())
167
+ activateProjectedElement(backSource);
168
+ }, { capture: true });
169
+ root.append(backProjection);
170
+ nextBack.setAttribute(marker, '');
171
+ nextBack.setAttribute('aria-hidden', 'true');
172
+ nextBack.dispatchEvent(new CustomEvent('nativeUIShellChange'));
173
+ topOffset = toolbarControlSize + toolbarControlGap;
174
+ }
175
+ for (const { group, actions: sources } of groups) {
176
+ const projection = (sources.length === 1 ? sources[0] : group).cloneNode(false);
177
+ if (sources.length === 1)
178
+ syncAction(projection, sources[0]);
179
+ else
180
+ copyAttributes(projection, group);
181
+ projection.classList.add(toolbarProjectionClass, 'ion-cloned-element');
182
+ projection.style.setProperty('--ios-theme-vertical-bars-toolbar-offset', `${topOffset}px`);
183
+ const actions = sources.map((source) => {
184
+ const clone = sources.length === 1 ? projection : source.cloneNode(false);
185
+ if (sources.length > 1)
186
+ syncAction(clone, source);
187
+ clone.addEventListener('click', (event) => {
188
+ event.preventDefault();
189
+ event.stopImmediatePropagation();
190
+ if (isCurrentToolbarAction(source))
191
+ activateProjectedElement(source);
192
+ }, { capture: true });
193
+ if (sources.length > 1)
194
+ projection.append(clone);
195
+ const previousAriaHidden = source.getAttribute('aria-hidden');
196
+ source.setAttribute(marker, '');
197
+ source.setAttribute('aria-hidden', 'true');
198
+ source.dispatchEvent(new CustomEvent('nativeUIShellChange'));
199
+ return { source, projection: clone, previousAriaHidden };
200
+ });
201
+ root.append(projection);
202
+ toolbarProjections.push({ source: group, projection, actions });
203
+ topOffset += actions.length * toolbarControlSize + toolbarControlGap;
204
+ }
205
+ if (nextBack || toolbarProjections.length)
206
+ root.classList.add(readyClass);
207
+ sourceObserver = new MutationObserver((records) => {
208
+ if (records.some((record) => record.type !== 'attributes' || (![marker, 'aria-hidden'].includes(record.attributeName ?? '') && !prehideOnlyMutation(record))))
209
+ schedule();
210
+ });
211
+ if (nextBack?.shadowRoot)
212
+ sourceObserver.observe(nextBack.shadowRoot, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
213
+ for (const { group } of groups)
214
+ sourceObserver.observe(group, { subtree: true, childList: true, attributes: true, attributeOldValue: true });
215
+ };
216
+ const performUpdate = () => {
217
+ frame = 0;
218
+ const currentRoot = verticalBarsRoot();
219
+ if (observingVerticalBars !== !!currentRoot) {
220
+ observingVerticalBars = !!currentRoot;
221
+ observer.disconnect();
222
+ observer.observe(doc.documentElement, {
223
+ subtree: true,
224
+ childList: true,
225
+ attributes: true,
226
+ attributeOldValue: true,
227
+ attributeFilter: observingVerticalBars ? undefined : ['class'],
228
+ });
229
+ }
230
+ if (stopped || suspended || !currentRoot || !enabled())
231
+ return restore();
232
+ const nextBack = findBack();
233
+ const groups = findToolbarGroups();
234
+ if (!nextBack && !groups.length)
235
+ return restore();
236
+ if (sameSources(nextBack, groups))
237
+ return syncExisting();
238
+ restore();
239
+ project(nextBack, groups);
240
+ updates++;
241
+ };
242
+ const update = () => {
243
+ try {
244
+ performUpdate();
245
+ }
246
+ catch {
247
+ restore();
248
+ }
249
+ finally {
250
+ const pending = waiters;
251
+ waiters = [];
252
+ pending.forEach((resolve) => resolve());
253
+ }
254
+ };
255
+ const schedule = () => {
256
+ if (stopped)
257
+ return Promise.resolve();
258
+ const done = new Promise((resolve) => waiters.push(resolve));
259
+ if (!frame)
260
+ frame = win.requestAnimationFrame(update);
261
+ return done;
262
+ };
263
+ const observer = new MutationObserver((records) => {
264
+ const insideProjection = (target) => {
265
+ const element = target instanceof Element ? target : target.parentNode instanceof Element ? target.parentNode : undefined;
266
+ const rootNode = target.getRootNode();
267
+ const shadowHost = rootNode instanceof ShadowRoot ? rootNode.host : undefined;
268
+ return !![element, shadowHost].some((candidate) => candidate?.closest(`.${backProjectionClass}, .${toolbarProjectionClass}`));
269
+ };
270
+ const verticalBarsChanged = records.some((record) => (record.type === 'attributes' && record.target.matches('ion-app')) ||
271
+ (record.type === 'childList' &&
272
+ Array.from(record.addedNodes).some((node) => node instanceof Element &&
273
+ (node.matches('ion-app.ios-theme-vertical-bars') || !!node.querySelector('ion-app.ios-theme-vertical-bars')))));
274
+ if ((observingVerticalBars &&
275
+ records.some((record) => record.attributeName !== marker && !prehideOnlyMutation(record) && !insideProjection(record.target))) ||
276
+ (!observingVerticalBars && verticalBarsChanged && records.some((record) => !prehideOnlyMutation(record))))
277
+ schedule();
278
+ });
279
+ observer.observe(doc.documentElement, {
280
+ subtree: true,
281
+ childList: true,
282
+ attributes: true,
283
+ attributeOldValue: true,
284
+ attributeFilter: ['class'],
285
+ });
286
+ const pageLifecycle = (event) => {
287
+ const page = event.target;
288
+ if (!(page instanceof HTMLElement) || !page.matches('.ion-page'))
289
+ return;
290
+ if (event.type === VERTICAL_BARS_TRANSITION_CANCELED) {
291
+ const entering = event.detail?.entering;
292
+ verticalBarsPages.cancel(entering, page);
293
+ }
294
+ else
295
+ verticalBarsPages.lifecycle(event);
296
+ if (verticalBarsRoot())
297
+ schedule();
298
+ };
299
+ for (const name of ['ionViewWillEnter', 'ionViewWillLeave', 'ionViewDidEnter', 'ionViewDidLeave'])
300
+ doc.addEventListener(name, pageLifecycle, { capture: true, signal: listeners.signal });
301
+ doc.addEventListener(VERTICAL_BARS_TRANSITION_CANCELED, pageLifecycle, { capture: true, signal: listeners.signal });
302
+ for (const name of ['ionModalWillPresent', 'ionModalDidDismiss'])
303
+ doc.addEventListener(name, schedule, { capture: true, signal: listeners.signal });
304
+ win.addEventListener('nativeUIShellRefresh', schedule, { signal: listeners.signal });
305
+ schedule();
306
+ return {
307
+ getStatus: () => ({
308
+ state: stopped ? 'stopped' : 'web',
309
+ projected: Number(!!backSource) + toolbarProjections.reduce((count, group) => count + group.actions.length, 0),
310
+ updates,
311
+ }),
312
+ async suspend() {
313
+ suspended++;
314
+ restore();
315
+ let resumed = false;
316
+ return {
317
+ async resume() {
318
+ if (resumed)
319
+ return;
320
+ resumed = true;
321
+ suspended = Math.max(0, suspended - 1);
322
+ if (!suspended)
323
+ await schedule();
324
+ },
325
+ };
326
+ },
327
+ async destroy() {
328
+ if (stopped)
329
+ return;
330
+ stopped = true;
331
+ win.cancelAnimationFrame(frame);
332
+ frame = 0;
333
+ waiters.forEach((resolve) => resolve());
334
+ waiters = [];
335
+ observer.disconnect();
336
+ sourceObserver?.disconnect();
337
+ listeners.abort();
338
+ restore();
339
+ },
340
+ };
341
+ };