@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.
Files changed (158) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +241 -0
  3. package/dist/css/components/ion-action-sheet.css +1 -0
  4. package/dist/css/components/ion-alert.css +1 -0
  5. package/dist/css/components/ion-button.css +1 -0
  6. package/dist/css/components/ion-card.css +1 -0
  7. package/dist/css/components/ion-content.css +1 -0
  8. package/dist/css/components/ion-datetime.css +1 -0
  9. package/dist/css/components/ion-fab.css +1 -0
  10. package/dist/css/components/ion-inputs.css +1 -0
  11. package/dist/css/components/ion-list.css +1 -0
  12. package/dist/css/components/ion-loading.css +1 -0
  13. package/dist/css/components/ion-menu.css +1 -0
  14. package/dist/css/components/ion-modal.css +1 -0
  15. package/dist/css/components/ion-picker.css +1 -0
  16. package/dist/css/components/ion-popover.css +1 -0
  17. package/dist/css/components/ion-range.css +1 -0
  18. package/dist/css/components/ion-searchbar.css +1 -0
  19. package/dist/css/components/ion-segment.css +1 -0
  20. package/dist/css/components/ion-tabs.css +1 -0
  21. package/dist/css/components/ion-toast.css +1 -0
  22. package/dist/css/components/ion-toggle.css +1 -0
  23. package/dist/css/components/ion-toolbar.css +1 -0
  24. package/dist/css/default-variables.css +1 -0
  25. package/dist/css/ionic-theme-ios26-dark-always.css +1 -0
  26. package/dist/css/ionic-theme-ios26-dark-class.css +1 -0
  27. package/dist/css/ionic-theme-ios26-dark-system.css +1 -0
  28. package/dist/css/ionic-theme-ios26.css +1 -0
  29. package/dist/css/md-ion-list-inset.css +1 -0
  30. package/dist/css/md-remove-ios-class-effect.css +1 -0
  31. package/dist/css/utils/api.css +1 -0
  32. package/dist/css/utils/dark/ion-button.css +1 -0
  33. package/dist/css/utils/dark/ion-fab.css +1 -0
  34. package/dist/css/utils/dark/ion-segment.css +1 -0
  35. package/dist/css/utils/dark/ion-tabs.css +1 -0
  36. package/dist/css/utils/searchable.css +1 -0
  37. package/dist/css/utils/structured-list.css +1 -0
  38. package/dist/css/utils/theme-dark.css +1 -0
  39. package/dist/css/utils/translucent.css +1 -0
  40. package/dist/docs.json +234 -0
  41. package/dist/focus-controller/index.d.ts +6 -0
  42. package/dist/focus-controller/index.d.ts.map +1 -0
  43. package/dist/focus-controller/index.js +62 -0
  44. package/dist/focus-controller /index.d.ts +6 -0
  45. package/dist/focus-controller /index.d.ts.map +1 -0
  46. package/dist/focus-controller /index.js +62 -0
  47. package/dist/index.d.ts +9 -0
  48. package/dist/index.d.ts.map +1 -0
  49. package/dist/index.js +30 -0
  50. package/dist/popover/animations/ios.enter.d.ts +4 -0
  51. package/dist/popover/animations/ios.enter.d.ts.map +1 -0
  52. package/dist/popover/animations/ios.enter.js +97 -0
  53. package/dist/popover/animations/ios.leave.d.ts +3 -0
  54. package/dist/popover/animations/ios.leave.d.ts.map +1 -0
  55. package/dist/popover/animations/ios.leave.js +60 -0
  56. package/dist/popover/popover-interface.d.ts +38 -0
  57. package/dist/popover/popover-interface.d.ts.map +1 -0
  58. package/dist/popover/popover-interface.js +1 -0
  59. package/dist/popover/utils.d.ts +37 -0
  60. package/dist/popover/utils.d.ts.map +1 -0
  61. package/dist/popover/utils.js +377 -0
  62. package/dist/sheets-of-glass/animations.d.ts +8 -0
  63. package/dist/sheets-of-glass/animations.d.ts.map +1 -0
  64. package/dist/sheets-of-glass/animations.js +97 -0
  65. package/dist/sheets-of-glass/index.d.ts +3 -0
  66. package/dist/sheets-of-glass/index.d.ts.map +1 -0
  67. package/dist/sheets-of-glass/index.js +174 -0
  68. package/dist/sheets-of-glass/interfaces.d.ts +16 -0
  69. package/dist/sheets-of-glass/interfaces.d.ts.map +1 -0
  70. package/dist/sheets-of-glass/interfaces.js +1 -0
  71. package/dist/tab-bar-searchable/animations/enter.d.ts +8 -0
  72. package/dist/tab-bar-searchable/animations/enter.d.ts.map +1 -0
  73. package/dist/tab-bar-searchable/animations/enter.js +73 -0
  74. package/dist/tab-bar-searchable/animations/leave.d.ts +8 -0
  75. package/dist/tab-bar-searchable/animations/leave.d.ts.map +1 -0
  76. package/dist/tab-bar-searchable/animations/leave.js +66 -0
  77. package/dist/tab-bar-searchable/index.d.ts +4 -0
  78. package/dist/tab-bar-searchable/index.d.ts.map +1 -0
  79. package/dist/tab-bar-searchable/index.js +75 -0
  80. package/dist/tab-bar-searchable/interfaces.d.ts +41 -0
  81. package/dist/tab-bar-searchable/interfaces.d.ts.map +1 -0
  82. package/dist/tab-bar-searchable/interfaces.js +5 -0
  83. package/dist/tab-bar-searchable/utils.d.ts +12 -0
  84. package/dist/tab-bar-searchable/utils.d.ts.map +1 -0
  85. package/dist/tab-bar-searchable/utils.js +60 -0
  86. package/dist/transition/index.d.ts +19 -0
  87. package/dist/transition/index.d.ts.map +1 -0
  88. package/dist/transition/index.js +219 -0
  89. package/dist/transition/ios.transition.d.ts +5 -0
  90. package/dist/transition/ios.transition.d.ts.map +1 -0
  91. package/dist/transition/ios.transition.js +609 -0
  92. package/dist/utils.d.ts +17 -0
  93. package/dist/utils.d.ts.map +1 -0
  94. package/dist/utils.js +69 -0
  95. package/docs/e2e-testing.md +51 -0
  96. package/docs/eslint.md +73 -0
  97. package/docs/experimental-animation.md +85 -0
  98. package/docs/features.md +88 -0
  99. package/docs/feedback.md +65 -0
  100. package/docs/ios-18.md +16 -0
  101. package/docs/migration.md +94 -0
  102. package/docs/special-markup.md +145 -0
  103. package/docs/using-ion-item-group.md +47 -0
  104. package/package.json +63 -0
  105. package/src/focus-controller/index.ts +123 -0
  106. package/src/index.ts +33 -0
  107. package/src/popover/animations/ios.enter.ts +144 -0
  108. package/src/popover/animations/ios.leave.ts +76 -0
  109. package/src/popover/popover-interface.ts +44 -0
  110. package/src/popover/utils.ts +696 -0
  111. package/src/sheets-of-glass/animations.ts +120 -0
  112. package/src/sheets-of-glass/index.ts +214 -0
  113. package/src/sheets-of-glass/interfaces.ts +17 -0
  114. package/src/styles/components/ion-action-sheet.scss +85 -0
  115. package/src/styles/components/ion-alert.scss +66 -0
  116. package/src/styles/components/ion-button.scss +443 -0
  117. package/src/styles/components/ion-card.scss +7 -0
  118. package/src/styles/components/ion-content.scss +22 -0
  119. package/src/styles/components/ion-datetime.scss +9 -0
  120. package/src/styles/components/ion-fab.scss +104 -0
  121. package/src/styles/components/ion-inputs.scss +82 -0
  122. package/src/styles/components/ion-list.scss +44 -0
  123. package/src/styles/components/ion-loading.scss +11 -0
  124. package/src/styles/components/ion-menu.scss +110 -0
  125. package/src/styles/components/ion-modal.scss +42 -0
  126. package/src/styles/components/ion-picker.scss +4 -0
  127. package/src/styles/components/ion-popover.scss +33 -0
  128. package/src/styles/components/ion-range.scss +90 -0
  129. package/src/styles/components/ion-searchbar.scss +68 -0
  130. package/src/styles/components/ion-segment.scss +150 -0
  131. package/src/styles/components/ion-tabs.scss +118 -0
  132. package/src/styles/components/ion-toast.scss +35 -0
  133. package/src/styles/components/ion-toggle.scss +65 -0
  134. package/src/styles/components/ion-toolbar.scss +76 -0
  135. package/src/styles/default-variables.scss +38 -0
  136. package/src/styles/ionic-theme-ios26-dark-always.scss +6 -0
  137. package/src/styles/ionic-theme-ios26-dark-class.scss +6 -0
  138. package/src/styles/ionic-theme-ios26-dark-system.scss +8 -0
  139. package/src/styles/ionic-theme-ios26.scss +23 -0
  140. package/src/styles/md-ion-list-inset.scss +22 -0
  141. package/src/styles/md-remove-ios-class-effect.scss +14 -0
  142. package/src/styles/utils/api.scss +146 -0
  143. package/src/styles/utils/dark/ion-button.scss +58 -0
  144. package/src/styles/utils/dark/ion-fab.scss +18 -0
  145. package/src/styles/utils/dark/ion-segment.scss +12 -0
  146. package/src/styles/utils/dark/ion-tabs.scss +21 -0
  147. package/src/styles/utils/searchable.scss +0 -0
  148. package/src/styles/utils/structured-list.scss +170 -0
  149. package/src/styles/utils/theme-dark.scss +25 -0
  150. package/src/styles/utils/translucent.scss +95 -0
  151. package/src/tab-bar-searchable/animations/enter.ts +92 -0
  152. package/src/tab-bar-searchable/animations/leave.ts +89 -0
  153. package/src/tab-bar-searchable/index.ts +141 -0
  154. package/src/tab-bar-searchable/interfaces.ts +28 -0
  155. package/src/tab-bar-searchable/utils.ts +75 -0
  156. package/src/transition/index.ts +369 -0
  157. package/src/transition/ios.transition.ts +966 -0
  158. package/src/utils.ts +91 -0
@@ -0,0 +1,696 @@
1
+ import { getElementRoot, raf } from '../utils';
2
+
3
+ import type { PopoverSize, PositionAlign, PositionReference, PositionSide, TriggerAction } from './popover-interface';
4
+ import { POPOVER_IOS_BODY_MARGIN } from './animations/ios.enter';
5
+
6
+ interface InteractionCallback {
7
+ eventName: string;
8
+ callback: (ev: any) => void; // TODO(FW-2832): type
9
+ }
10
+
11
+ export interface ReferenceCoordinates {
12
+ top: number;
13
+ left: number;
14
+ width: number;
15
+ height: number;
16
+ }
17
+
18
+ interface PopoverPosition {
19
+ top: number;
20
+ left: number;
21
+ referenceCoordinates?: ReferenceCoordinates;
22
+ originX: string;
23
+ originY: string;
24
+ }
25
+
26
+ export interface PopoverStyles {
27
+ top: number;
28
+ left: number;
29
+ bottom?: number;
30
+ originX: string;
31
+ originY: string;
32
+ checkSafeAreaLeft: boolean;
33
+ checkSafeAreaRight: boolean;
34
+ addPopoverBottomClass: boolean;
35
+ }
36
+
37
+ /**
38
+ * Returns the recommended dimensions of the popover
39
+ * that takes into account whether or not the width
40
+ * should match the trigger width.
41
+ */
42
+ export const getPopoverDimensions = (size: PopoverSize, contentEl: HTMLElement, triggerEl?: HTMLElement) => {
43
+ const contentDimentions = contentEl.getBoundingClientRect();
44
+ const contentHeight = contentDimentions.height;
45
+ let contentWidth = contentDimentions.width;
46
+
47
+ if (size === 'cover' && triggerEl) {
48
+ const triggerDimensions = triggerEl.getBoundingClientRect();
49
+ contentWidth = triggerDimensions.width;
50
+ }
51
+
52
+ return {
53
+ contentWidth,
54
+ contentHeight,
55
+ };
56
+ };
57
+
58
+ export const configureDismissInteraction = (
59
+ triggerEl: HTMLElement,
60
+ triggerAction: TriggerAction,
61
+ popoverEl: HTMLIonPopoverElement,
62
+ parentPopoverEl: HTMLIonPopoverElement,
63
+ ) => {
64
+ let dismissCallbacks: InteractionCallback[] = [];
65
+ const root = getElementRoot(parentPopoverEl);
66
+ const parentContentEl = root.querySelector('.popover-content') as HTMLElement;
67
+
68
+ switch (triggerAction) {
69
+ case 'hover':
70
+ dismissCallbacks = [
71
+ {
72
+ /**
73
+ * Do not use mouseover here
74
+ * as this will causes the event to
75
+ * be dispatched on each underlying
76
+ * element rather than on the popover
77
+ * content as a whole.
78
+ */
79
+ eventName: 'mouseenter',
80
+ callback: (ev: MouseEvent) => {
81
+ /**
82
+ * Do not dismiss the popover is we
83
+ * are hovering over its trigger.
84
+ * This would be easier if we used mouseover
85
+ * but this would cause the event to be dispatched
86
+ * more often than we would like, potentially
87
+ * causing performance issues.
88
+ */
89
+ const element = document.elementFromPoint(ev.clientX, ev.clientY) as HTMLElement | null;
90
+ if (element === triggerEl) {
91
+ return;
92
+ }
93
+
94
+ popoverEl.dismiss(undefined, undefined, false);
95
+ },
96
+ },
97
+ ];
98
+ break;
99
+ case 'context-menu':
100
+ case 'click':
101
+ default:
102
+ dismissCallbacks = [
103
+ {
104
+ eventName: 'click',
105
+ callback: (ev: MouseEvent) => {
106
+ /**
107
+ * Do not dismiss the popover is we
108
+ * are hovering over its trigger.
109
+ */
110
+ const target = ev.target as HTMLElement;
111
+ const closestTrigger = target.closest('[data-ion-popover-trigger]');
112
+ if (closestTrigger === triggerEl) {
113
+ /**
114
+ * stopPropagation here so if the
115
+ * popover has dismissOnSelect="true"
116
+ * the popover does not dismiss since
117
+ * we just clicked a trigger element.
118
+ */
119
+ ev.stopPropagation();
120
+ return;
121
+ }
122
+
123
+ popoverEl.dismiss(undefined, undefined, false);
124
+ },
125
+ },
126
+ ];
127
+ break;
128
+ }
129
+
130
+ dismissCallbacks.forEach(({ eventName, callback }) => parentContentEl.addEventListener(eventName, callback));
131
+
132
+ return () => {
133
+ dismissCallbacks.forEach(({ eventName, callback }) => parentContentEl.removeEventListener(eventName, callback));
134
+ };
135
+ };
136
+
137
+ /**
138
+ * Configures the triggerEl to respond
139
+ * to user interaction based upon the triggerAction
140
+ * prop that devs have defined.
141
+ */
142
+ export const configureTriggerInteraction = (triggerEl: HTMLElement, triggerAction: TriggerAction, popoverEl: HTMLIonPopoverElement) => {
143
+ let triggerCallbacks: InteractionCallback[] = [];
144
+
145
+ /**
146
+ * Based upon the kind of trigger interaction
147
+ * the user wants, we setup the correct event
148
+ * listeners.
149
+ */
150
+ switch (triggerAction) {
151
+ case 'hover':
152
+ let hoverTimeout: ReturnType<typeof setTimeout> | undefined;
153
+
154
+ triggerCallbacks = [
155
+ {
156
+ eventName: 'mouseenter',
157
+ callback: async (ev: Event) => {
158
+ ev.stopPropagation();
159
+
160
+ if (hoverTimeout) {
161
+ clearTimeout(hoverTimeout);
162
+ }
163
+
164
+ /**
165
+ * Hovering over a trigger should not
166
+ * immediately open the next popover.
167
+ */
168
+ hoverTimeout = setTimeout(() => {
169
+ raf(() => {
170
+ popoverEl.presentFromTrigger(ev);
171
+ hoverTimeout = undefined;
172
+ });
173
+ }, 100);
174
+ },
175
+ },
176
+ {
177
+ eventName: 'mouseleave',
178
+ callback: (ev: MouseEvent) => {
179
+ if (hoverTimeout) {
180
+ clearTimeout(hoverTimeout);
181
+ }
182
+
183
+ /**
184
+ * If mouse is over another popover
185
+ * that is not this popover then we should
186
+ * close this popover.
187
+ */
188
+ const target = ev.relatedTarget as HTMLElement | null;
189
+ if (!target) {
190
+ return;
191
+ }
192
+
193
+ if (target.closest('ion-popover') !== popoverEl) {
194
+ popoverEl.dismiss(undefined, undefined, false);
195
+ }
196
+ },
197
+ },
198
+ {
199
+ /**
200
+ * stopPropagation here prevents the popover
201
+ * from dismissing when dismiss-on-select="true".
202
+ */
203
+ eventName: 'click',
204
+ callback: (ev: Event) => ev.stopPropagation(),
205
+ },
206
+ {
207
+ eventName: 'ionPopoverActivateTrigger',
208
+ callback: (ev: Event) => popoverEl.presentFromTrigger(ev, true),
209
+ },
210
+ ];
211
+
212
+ break;
213
+ case 'context-menu':
214
+ triggerCallbacks = [
215
+ {
216
+ eventName: 'contextmenu',
217
+ callback: (ev: Event) => {
218
+ /**
219
+ * Prevents the platform context
220
+ * menu from appearing.
221
+ */
222
+ ev.preventDefault();
223
+ popoverEl.presentFromTrigger(ev);
224
+ },
225
+ },
226
+ {
227
+ eventName: 'click',
228
+ callback: (ev: Event) => ev.stopPropagation(),
229
+ },
230
+ {
231
+ eventName: 'ionPopoverActivateTrigger',
232
+ callback: (ev: Event) => popoverEl.presentFromTrigger(ev, true),
233
+ },
234
+ ];
235
+
236
+ break;
237
+ case 'click':
238
+ default:
239
+ triggerCallbacks = [
240
+ {
241
+ /**
242
+ * Do not do a stopPropagation() here
243
+ * because if you had two click triggers
244
+ * then clicking the first trigger and then
245
+ * clicking the second trigger would not cause
246
+ * the first popover to dismiss.
247
+ */
248
+ eventName: 'click',
249
+ callback: (ev: Event) => popoverEl.presentFromTrigger(ev),
250
+ },
251
+ {
252
+ eventName: 'ionPopoverActivateTrigger',
253
+ callback: (ev: Event) => popoverEl.presentFromTrigger(ev, true),
254
+ },
255
+ ];
256
+ break;
257
+ }
258
+
259
+ triggerCallbacks.forEach(({ eventName, callback }) => triggerEl.addEventListener(eventName, callback));
260
+ triggerEl.setAttribute('data-ion-popover-trigger', 'true');
261
+
262
+ return () => {
263
+ triggerCallbacks.forEach(({ eventName, callback }) => triggerEl.removeEventListener(eventName, callback));
264
+ triggerEl.removeAttribute('data-ion-popover-trigger');
265
+ };
266
+ };
267
+
268
+ /**
269
+ * Returns the index of an ion-item in an array of ion-items.
270
+ */
271
+ export const getIndexOfItem = (items: HTMLIonItemElement[], item: HTMLElement | null) => {
272
+ if (!item || item.tagName !== 'ION-ITEM') {
273
+ return -1;
274
+ }
275
+
276
+ return items.findIndex((el) => el === item);
277
+ };
278
+
279
+ /**
280
+ * Given an array of elements and a currently focused ion-item
281
+ * returns the next ion-item relative to the focused one or
282
+ * undefined.
283
+ */
284
+ export const getNextItem = (items: HTMLIonItemElement[], currentItem: HTMLElement | null) => {
285
+ const currentItemIndex = getIndexOfItem(items, currentItem);
286
+ return items[currentItemIndex + 1];
287
+ };
288
+
289
+ /**
290
+ * Given an array of elements and a currently focused ion-item
291
+ * returns the previous ion-item relative to the focused one or
292
+ * undefined.
293
+ */
294
+ export const getPrevItem = (items: HTMLIonItemElement[], currentItem: HTMLElement | null) => {
295
+ const currentItemIndex = getIndexOfItem(items, currentItem);
296
+ return items[currentItemIndex - 1];
297
+ };
298
+
299
+ /** Focus the internal button of the ion-item */
300
+ const focusItem = (item: HTMLIonItemElement) => {
301
+ const root = getElementRoot(item);
302
+ const button = root.querySelector('button');
303
+
304
+ if (button) {
305
+ raf(() => button.focus());
306
+ }
307
+ };
308
+
309
+ /**
310
+ * Positions a popover by taking into account
311
+ * the reference point, preferred side, alignment
312
+ * and viewport dimensions.
313
+ */
314
+ export const getPopoverPosition = (
315
+ isRTL: boolean,
316
+ contentWidth: number,
317
+ contentHeight: number,
318
+ reference: PositionReference,
319
+ side: PositionSide,
320
+ align: PositionAlign,
321
+ defaultPosition: PopoverPosition,
322
+ triggerEl?: HTMLElement,
323
+ event?: MouseEvent | CustomEvent,
324
+ ): PopoverPosition => {
325
+ let referenceCoordinates = {
326
+ top: 0,
327
+ left: 0,
328
+ width: 0,
329
+ height: 0,
330
+ };
331
+
332
+ /**
333
+ * Calculate position relative to the
334
+ * x-y coordinates in the event that
335
+ * was passed in
336
+ */
337
+ switch (reference) {
338
+ case 'event':
339
+ if (!event) {
340
+ return defaultPosition;
341
+ }
342
+
343
+ const mouseEv = event as MouseEvent;
344
+
345
+ referenceCoordinates = {
346
+ top: mouseEv.clientY,
347
+ left: mouseEv.clientX,
348
+ width: 1,
349
+ height: 1,
350
+ };
351
+
352
+ break;
353
+
354
+ /**
355
+ * Calculate position relative to the bounding
356
+ * box on either the trigger element
357
+ * specified via the `trigger` prop or
358
+ * the target specified on the event
359
+ * that was passed in.
360
+ */
361
+ case 'trigger':
362
+ default:
363
+ const customEv = event as CustomEvent;
364
+
365
+ /**
366
+ * ionShadowTarget is used when we need to align the
367
+ * popover with an element inside of the shadow root
368
+ * of an Ionic component. Ex: Presenting a popover
369
+ * by clicking on the collapsed indicator inside
370
+ * of `ion-breadcrumb` and centering it relative
371
+ * to the indicator rather than `ion-breadcrumb`
372
+ * as a whole.
373
+ */
374
+ const actualTriggerEl = (triggerEl || customEv?.detail?.ionShadowTarget || customEv?.target) as HTMLElement | null;
375
+ if (!actualTriggerEl) {
376
+ return defaultPosition;
377
+ }
378
+ const triggerBoundingBox = actualTriggerEl.getBoundingClientRect();
379
+ referenceCoordinates = {
380
+ top: triggerBoundingBox.top,
381
+ left: triggerBoundingBox.left,
382
+ width: triggerBoundingBox.width,
383
+ height: triggerBoundingBox.height,
384
+ };
385
+
386
+ break;
387
+ }
388
+
389
+ /**
390
+ * Get top/left offset that would allow
391
+ * popover to be positioned on the
392
+ * preferred side of the reference.
393
+ */
394
+ const coordinates = calculatePopoverSide(side, referenceCoordinates, contentWidth, contentHeight, isRTL);
395
+
396
+ /**
397
+ * Get the top/left adjustments that
398
+ * would allow the popover content
399
+ * to have the correct alignment.
400
+ */
401
+ const alignedCoordinates = calculatePopoverAlign(align, side, referenceCoordinates, contentWidth, contentHeight);
402
+
403
+ const top = coordinates.top + alignedCoordinates.top;
404
+ const left = coordinates.left + alignedCoordinates.left;
405
+
406
+ const { originX, originY } = calculatePopoverOrigin(side, align, isRTL);
407
+
408
+ return { top, left, referenceCoordinates, originX, originY };
409
+ };
410
+
411
+ /**
412
+ * Determines the transform-origin
413
+ * of the popover animation so that it
414
+ * is in line with what the side and alignment
415
+ * prop values are. Currently only used
416
+ * with the MD animation.
417
+ */
418
+ const calculatePopoverOrigin = (side: PositionSide, align: PositionAlign, isRTL: boolean) => {
419
+ switch (side) {
420
+ case 'top':
421
+ return { originX: getOriginXAlignment(align), originY: 'bottom' };
422
+ case 'bottom':
423
+ return { originX: getOriginXAlignment(align), originY: 'top' };
424
+ case 'left':
425
+ return { originX: 'right', originY: getOriginYAlignment(align) };
426
+ case 'right':
427
+ return { originX: 'left', originY: getOriginYAlignment(align) };
428
+ case 'start':
429
+ return { originX: isRTL ? 'left' : 'right', originY: getOriginYAlignment(align) };
430
+ case 'end':
431
+ return { originX: isRTL ? 'right' : 'left', originY: getOriginYAlignment(align) };
432
+ }
433
+ };
434
+
435
+ const getOriginXAlignment = (align: PositionAlign) => {
436
+ switch (align) {
437
+ case 'start':
438
+ return 'left';
439
+ case 'center':
440
+ return 'center';
441
+ case 'end':
442
+ return 'right';
443
+ }
444
+ };
445
+
446
+ const getOriginYAlignment = (align: PositionAlign) => {
447
+ switch (align) {
448
+ case 'start':
449
+ return 'top';
450
+ case 'center':
451
+ return 'center';
452
+ case 'end':
453
+ return 'bottom';
454
+ }
455
+ };
456
+
457
+ /**
458
+ * Calculates the required top/left
459
+ * values needed to position the popover
460
+ * content on the side specified in the
461
+ * `side` prop.
462
+ */
463
+ const calculatePopoverSide = (
464
+ side: PositionSide,
465
+ triggerBoundingBox: ReferenceCoordinates,
466
+ contentWidth: number,
467
+ contentHeight: number,
468
+ isRTL: boolean,
469
+ ) => {
470
+ const sideLeft = {
471
+ top: triggerBoundingBox.top,
472
+ left: triggerBoundingBox.left - contentWidth,
473
+ };
474
+ const sideRight = {
475
+ top: triggerBoundingBox.top,
476
+ left: triggerBoundingBox.left + triggerBoundingBox.width,
477
+ };
478
+
479
+ switch (side) {
480
+ case 'top':
481
+ return {
482
+ top: triggerBoundingBox.top - contentHeight,
483
+ left: triggerBoundingBox.left,
484
+ };
485
+ case 'right':
486
+ return sideRight;
487
+ case 'bottom':
488
+ return {
489
+ top: triggerBoundingBox.top + triggerBoundingBox.height,
490
+ left: triggerBoundingBox.left,
491
+ };
492
+ case 'left':
493
+ return sideLeft;
494
+ case 'start':
495
+ return isRTL ? sideRight : sideLeft;
496
+ case 'end':
497
+ return isRTL ? sideLeft : sideRight;
498
+ }
499
+ };
500
+
501
+ /**
502
+ * Calculates the required top/left
503
+ * offset values needed to provide the
504
+ * correct alignment regardless while taking
505
+ * into account the side the popover is on.
506
+ */
507
+ const calculatePopoverAlign = (
508
+ align: PositionAlign,
509
+ side: PositionSide,
510
+ triggerBoundingBox: ReferenceCoordinates,
511
+ contentWidth: number,
512
+ contentHeight: number,
513
+ ) => {
514
+ switch (align) {
515
+ case 'center':
516
+ return calculatePopoverCenterAlign(side, triggerBoundingBox, contentWidth, contentHeight);
517
+ case 'end':
518
+ return calculatePopoverEndAlign(side, triggerBoundingBox, contentWidth, contentHeight);
519
+ case 'start':
520
+ default:
521
+ return { top: 0, left: 0 };
522
+ }
523
+ };
524
+
525
+ /**
526
+ * Calculate the end alignment for
527
+ * the popover. If side is on the x-axis
528
+ * then the align values refer to the top
529
+ * and bottom margins of the content.
530
+ * If side is on the y-axis then the
531
+ * align values refer to the left and right
532
+ * margins of the content.
533
+ */
534
+ const calculatePopoverEndAlign = (
535
+ side: PositionSide,
536
+ triggerBoundingBox: ReferenceCoordinates,
537
+ contentWidth: number,
538
+ contentHeight: number,
539
+ ) => {
540
+ switch (side) {
541
+ case 'start':
542
+ case 'end':
543
+ case 'left':
544
+ case 'right':
545
+ return {
546
+ top: -(contentHeight - triggerBoundingBox.height),
547
+ left: 0,
548
+ };
549
+ case 'top':
550
+ case 'bottom':
551
+ default:
552
+ return {
553
+ top: 0,
554
+ left: -(contentWidth - triggerBoundingBox.width),
555
+ };
556
+ }
557
+ };
558
+
559
+ /**
560
+ * Calculate the center alignment for
561
+ * the popover. If side is on the x-axis
562
+ * then the align values refer to the top
563
+ * and bottom margins of the content.
564
+ * If side is on the y-axis then the
565
+ * align values refer to the left and right
566
+ * margins of the content.
567
+ */
568
+ const calculatePopoverCenterAlign = (
569
+ side: PositionSide,
570
+ triggerBoundingBox: ReferenceCoordinates,
571
+ contentWidth: number,
572
+ contentHeight: number,
573
+ ) => {
574
+ switch (side) {
575
+ case 'start':
576
+ case 'end':
577
+ case 'left':
578
+ case 'right':
579
+ return {
580
+ top: -(contentHeight / 2 - triggerBoundingBox.height / 2),
581
+ left: 0,
582
+ };
583
+ case 'top':
584
+ case 'bottom':
585
+ default:
586
+ return {
587
+ top: 0,
588
+ left: -(contentWidth / 2 - triggerBoundingBox.width / 2),
589
+ };
590
+ }
591
+ };
592
+
593
+ /**
594
+ * Adjusts popover positioning coordinates
595
+ * such that popover does not appear offscreen
596
+ * or overlapping safe area bounds.
597
+ */
598
+ export const calculateWindowAdjustment = (
599
+ side: PositionSide,
600
+ coordTop: number,
601
+ coordLeft: number,
602
+ bodyPadding: number,
603
+ bodyWidth: number,
604
+ bodyHeight: number,
605
+ contentWidth: number,
606
+ contentHeight: number,
607
+ safeAreaMargin: number,
608
+ contentOriginX: string,
609
+ contentOriginY: string,
610
+ triggerCoordinates?: ReferenceCoordinates,
611
+ eventElementRect?: DOMRect,
612
+ isReplace: boolean = false,
613
+ ): PopoverStyles => {
614
+ const triggerTop = triggerCoordinates ? triggerCoordinates.top + triggerCoordinates.height : bodyHeight / 2 - contentHeight / 2;
615
+ const triggerHeight = triggerCoordinates ? triggerCoordinates.height : 0;
616
+ let left = coordLeft;
617
+ let top = !isReplace ? coordTop + POPOVER_IOS_BODY_MARGIN : coordTop - triggerHeight;
618
+ let bottom;
619
+ let originX = contentOriginX;
620
+ let originY = contentOriginY;
621
+ let checkSafeAreaLeft = false;
622
+ let checkSafeAreaRight = false;
623
+ let addPopoverBottomClass = false;
624
+
625
+ /**
626
+ * Adjust popover so it does not
627
+ * go off the left of the screen.
628
+ */
629
+ if (left < bodyPadding + safeAreaMargin) {
630
+ left = !eventElementRect ? bodyPadding : eventElementRect.left;
631
+ if (left === 0) {
632
+ left = safeAreaMargin;
633
+ }
634
+ checkSafeAreaLeft = true;
635
+ originX = 'left';
636
+ /**
637
+ * Adjust popover so it does not
638
+ * go off the right of the screen.
639
+ */
640
+ } else if (contentWidth + bodyPadding + left + safeAreaMargin > bodyWidth) {
641
+ checkSafeAreaRight = true;
642
+ left = !eventElementRect ? bodyWidth - contentWidth - bodyPadding : eventElementRect.right - contentWidth;
643
+ if (left + contentWidth === bodyWidth) {
644
+ left = left - safeAreaMargin;
645
+ }
646
+ originX = 'right';
647
+ }
648
+
649
+ /**
650
+ * Adjust popover so it does not
651
+ * go off the top of the screen.
652
+ * If popover is on the left or the right of
653
+ * the trigger, then we should not adjust top
654
+ * margins.
655
+ */
656
+ const compareTop = triggerCoordinates ? triggerCoordinates.top + triggerCoordinates.height / 2 : bodyHeight / 2 - contentHeight / 2;
657
+ if (compareTop > bodyHeight / 2 && (side === 'top' || side === 'bottom')) {
658
+ if (triggerTop - contentHeight > 0) {
659
+ /**
660
+ * While we strive to align the popover with the trigger
661
+ * on smaller screens this is not always possible. As a result,
662
+ * we adjust the popover up so that it does not hang
663
+ * off the bottom of the screen. However, we do not want to move
664
+ * the popover up so much that it goes off the top of the screen.
665
+ *
666
+ * We chose 12 here so that the popover position looks a bit nicer as
667
+ * it is not right up against the edge of the screen.
668
+ */
669
+ if (!isReplace) {
670
+ top = Math.max(12, triggerTop - contentHeight - triggerHeight) - POPOVER_IOS_BODY_MARGIN;
671
+ } else {
672
+ top = Math.max(12, triggerTop - contentHeight);
673
+ }
674
+ originY = 'bottom';
675
+ addPopoverBottomClass = true;
676
+
677
+ /**
678
+ * If not enough room for popover to appear
679
+ * above trigger, then cut it off.
680
+ */
681
+ } else {
682
+ bottom = bodyPadding;
683
+ }
684
+ }
685
+
686
+ return {
687
+ top,
688
+ left,
689
+ bottom,
690
+ originX,
691
+ originY,
692
+ checkSafeAreaLeft,
693
+ checkSafeAreaRight,
694
+ addPopoverBottomClass,
695
+ };
696
+ };