@ionic/core 8.8.7-dev.11779385275.161a641b → 8.8.7-dev.11779467048.1641d05e
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/components/index.js +1 -1
- package/components/ion-action-sheet.js +1 -1
- package/components/ion-alert.js +1 -1
- package/components/ion-datetime.js +1 -1
- package/components/ion-gallery.js +1 -1
- package/components/ion-loading.js +1 -1
- package/components/ion-menu.js +1 -1
- package/components/ion-modal.js +1 -1
- package/components/ion-picker-legacy.js +1 -1
- package/components/ion-popover.js +1 -1
- package/components/ion-select-modal.js +1 -1
- package/components/ion-select-popover.js +1 -1
- package/components/ion-select.js +1 -1
- package/components/ion-toast.js +1 -1
- package/components/{p-B2rpt1JV.js → p-C38HUpU5.js} +1 -1
- package/components/{p-Dmuy6xyk.js → p-C4G6C9fP.js} +1 -1
- package/components/{p-B6zr9RZN.js → p-CVRxImH6.js} +1 -1
- package/components/{p-B71c6yUH.js → p-CoFDGTFO.js} +1 -1
- package/components/{p-DAv9P_LE.js → p-CykCvfXQ.js} +1 -1
- package/components/p-DHTe6lDL.js +4 -0
- package/components/{p-Di5rHO3q.js → p-qZr7hBPz.js} +1 -1
- package/dist/cjs/index.cjs.js +1 -1
- package/dist/cjs/ion-action-sheet.cjs.entry.js +1 -1
- package/dist/cjs/ion-alert.cjs.entry.js +1 -1
- package/dist/cjs/ion-datetime_3.cjs.entry.js +1 -1
- package/dist/cjs/ion-gallery.cjs.entry.js +7 -28
- package/dist/cjs/ion-loading.cjs.entry.js +1 -1
- package/dist/cjs/ion-menu_3.cjs.entry.js +1 -1
- package/dist/cjs/ion-modal.cjs.entry.js +1 -1
- package/dist/cjs/ion-popover.cjs.entry.js +1 -1
- package/dist/cjs/ion-select-modal.cjs.entry.js +1 -1
- package/dist/cjs/ion-select_3.cjs.entry.js +1 -1
- package/dist/cjs/ion-toast.cjs.entry.js +1 -1
- package/dist/cjs/{overlays-Hci_7vw_.js → overlays-C54DhaTC.js} +187 -10
- package/dist/collection/components/gallery/gallery.js +7 -28
- package/dist/collection/utils/overlays.js +187 -10
- package/dist/docs.json +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/ion-action-sheet.entry.js +1 -1
- package/dist/esm/ion-alert.entry.js +1 -1
- package/dist/esm/ion-datetime_3.entry.js +1 -1
- package/dist/esm/ion-gallery.entry.js +7 -28
- package/dist/esm/ion-loading.entry.js +1 -1
- package/dist/esm/ion-menu_3.entry.js +1 -1
- package/dist/esm/ion-modal.entry.js +1 -1
- package/dist/esm/ion-popover.entry.js +1 -1
- package/dist/esm/ion-select-modal.entry.js +1 -1
- package/dist/esm/ion-select_3.entry.js +1 -1
- package/dist/esm/ion-toast.entry.js +1 -1
- package/dist/esm/{overlays-rwDDzEs4.js → overlays-ttYCMKRp.js} +187 -10
- package/dist/ionic/index.esm.js +1 -1
- package/dist/ionic/ionic.esm.js +1 -1
- package/dist/ionic/p-06bd033b.entry.js +4 -0
- package/dist/ionic/{p-c10fa162.entry.js → p-1f74b8d4.entry.js} +1 -1
- package/dist/ionic/{p-a9fb086b.entry.js → p-2f8aa0ac.entry.js} +1 -1
- package/dist/ionic/{p-2f0073af.entry.js → p-3331cfa9.entry.js} +1 -1
- package/dist/ionic/{p-35b144f5.entry.js → p-33c34361.entry.js} +1 -1
- package/dist/ionic/{p-15e3e8f5.entry.js → p-5061a8d4.entry.js} +1 -1
- package/dist/ionic/{p-4a0260e6.entry.js → p-8f04bd89.entry.js} +1 -1
- package/dist/ionic/{p-bf972309.entry.js → p-967576f8.entry.js} +1 -1
- package/dist/ionic/p-DdyNaGpi.js +4 -0
- package/dist/ionic/{p-71b6014c.entry.js → p-bb898d47.entry.js} +1 -1
- package/dist/ionic/p-dea52cb3.entry.js +4 -0
- package/dist/ionic/{p-432c5888.entry.js → p-fc796d48.entry.js} +1 -1
- package/dist/types/components/gallery/gallery.d.ts +2 -5
- package/hydrate/index.js +194 -38
- package/hydrate/index.mjs +194 -38
- package/package.json +1 -1
- package/components/p-CtiqM786.js +0 -4
- package/dist/ionic/p-0f3b4262.entry.js +0 -4
- package/dist/ionic/p-4079cee3.entry.js +0 -4
- package/dist/ionic/p-C4uUM9DM.js +0 -4
|
@@ -104,8 +104,101 @@ const focusElementInContext = (hostToFocus, fallbackElement) => {
|
|
|
104
104
|
let lastOverlayIndex = 0;
|
|
105
105
|
let lastId = 0;
|
|
106
106
|
const activeAnimations = new WeakMap();
|
|
107
|
+
const OVERLAY_FOCUS_TRAP_SELECTOR = 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker-legacy,ion-popover';
|
|
108
|
+
const ION_SELECT_MODAL_SELECTOR = 'ion-select-modal';
|
|
109
|
+
const isSelectModalOptionControl = (el) => el.tagName === 'ION-RADIO' || el.tagName === 'ION-CHECKBOX';
|
|
107
110
|
/**
|
|
108
|
-
*
|
|
111
|
+
* Returns the currently focused element for keyboard focus checks.
|
|
112
|
+
*
|
|
113
|
+
* Starts from `document.activeElement` (non-shadow / light DOM focus).
|
|
114
|
+
* If focus is inside one or more open shadow roots
|
|
115
|
+
* (e.g. native control inside `ion-radio`), walks through nested
|
|
116
|
+
* `shadowRoot.activeElement` values until the innermost focused node is reached.
|
|
117
|
+
*/
|
|
118
|
+
const getActiveElement = (ownerDoc) => {
|
|
119
|
+
var _a;
|
|
120
|
+
let active = ownerDoc.activeElement;
|
|
121
|
+
if (!active) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
while ((_a = active.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement) {
|
|
125
|
+
active = active.shadowRoot.activeElement;
|
|
126
|
+
}
|
|
127
|
+
return active;
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Walks from a focused node (possibly deep inside shadow roots)
|
|
131
|
+
* up to the nearest `ion-radio` / `ion-checkbox` host.
|
|
132
|
+
*/
|
|
133
|
+
const getOptionControlHost = (active) => {
|
|
134
|
+
let n = active;
|
|
135
|
+
while (n) {
|
|
136
|
+
if (isSelectModalOptionControl(n)) {
|
|
137
|
+
return n;
|
|
138
|
+
}
|
|
139
|
+
const root = n.getRootNode();
|
|
140
|
+
if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
|
|
141
|
+
n = root.host;
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
};
|
|
149
|
+
/**
|
|
150
|
+
* Sheet modals can have visual order that differs from DOM order.
|
|
151
|
+
* Without sorting, Tab can skip the handle after option traversal.
|
|
152
|
+
*
|
|
153
|
+
* Order: option controls (radio/checkbox) → sheet handle → end-slot
|
|
154
|
+
* header button. Any other focusables are appended after.
|
|
155
|
+
*/
|
|
156
|
+
const sortSheetModalFocusables = (overlay, elements) => {
|
|
157
|
+
const optionControls = elements.filter((el) => {
|
|
158
|
+
return overlay.contains(el) && isSelectModalOptionControl(el) && el.tabIndex >= 0;
|
|
159
|
+
});
|
|
160
|
+
const cancelControl = elements.find((el) => overlay.contains(el) &&
|
|
161
|
+
el.tagName === 'ION-BUTTON' &&
|
|
162
|
+
el.closest('ion-header ion-buttons[slot="end"]') !== null);
|
|
163
|
+
const handleControl = elements.find((el) => el.classList.contains('modal-handle'));
|
|
164
|
+
const sortByGeometry = (els) => [...els].sort((a, b) => {
|
|
165
|
+
const ra = a.getBoundingClientRect();
|
|
166
|
+
const rb = b.getBoundingClientRect();
|
|
167
|
+
const topDiff = ra.top - rb.top;
|
|
168
|
+
if (Math.abs(topDiff) > 1) {
|
|
169
|
+
return topDiff;
|
|
170
|
+
}
|
|
171
|
+
return ra.left - rb.left;
|
|
172
|
+
});
|
|
173
|
+
const ordered = [];
|
|
174
|
+
ordered.push(...sortByGeometry(optionControls));
|
|
175
|
+
if (handleControl) {
|
|
176
|
+
ordered.push(handleControl);
|
|
177
|
+
}
|
|
178
|
+
if (cancelControl) {
|
|
179
|
+
ordered.push(cancelControl);
|
|
180
|
+
}
|
|
181
|
+
const used = new Set(ordered);
|
|
182
|
+
for (const el of elements) {
|
|
183
|
+
if (!used.has(el)) {
|
|
184
|
+
ordered.push(el);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return ordered;
|
|
188
|
+
};
|
|
189
|
+
/**
|
|
190
|
+
* Option controls in groups use a tabindex pattern where only one
|
|
191
|
+
* option is tabbable (`tabIndex="0"`) while the others are `-1`.
|
|
192
|
+
* This returns the index of that current tabbable option.
|
|
193
|
+
*/
|
|
194
|
+
const getTabbableOptionControlIndex = (elements, overlay) => {
|
|
195
|
+
return elements.findIndex((el) => {
|
|
196
|
+
return overlay.contains(el) && isSelectModalOptionControl(el) && el.tabIndex >= 0;
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* Determines if the overlay's backdrop is always blocking
|
|
201
|
+
* (no background interaction).
|
|
109
202
|
* Returns false if showBackdrop=false or backdropBreakpoint > 0.
|
|
110
203
|
*/
|
|
111
204
|
const isBackdropAlwaysBlocking = (el) => {
|
|
@@ -226,7 +319,7 @@ const focusElementInOverlay = (hostToFocus, overlay) => {
|
|
|
226
319
|
* Should NOT include: Toast
|
|
227
320
|
*/
|
|
228
321
|
const trapKeyboardFocus = (ev, doc) => {
|
|
229
|
-
const lastOverlay = getPresentedOverlay(doc,
|
|
322
|
+
const lastOverlay = getPresentedOverlay(doc, OVERLAY_FOCUS_TRAP_SELECTOR);
|
|
230
323
|
const target = ev.target;
|
|
231
324
|
/**
|
|
232
325
|
* If no active overlay, ignore this event.
|
|
@@ -409,6 +502,30 @@ const connectListeners = (doc) => {
|
|
|
409
502
|
doc.addEventListener('focus', (ev) => {
|
|
410
503
|
trapKeyboardFocus(ev, doc);
|
|
411
504
|
}, true);
|
|
505
|
+
/**
|
|
506
|
+
* Remember which option control last received focus
|
|
507
|
+
* (arrows, click, or Tab). This pattern keeps `tabIndex=0` on the
|
|
508
|
+
* checked/first radio, so the Tab trap uses this when wrapping back
|
|
509
|
+
* into the list or focusing the option-group slot.
|
|
510
|
+
*/
|
|
511
|
+
doc.addEventListener('focusin', (ev) => {
|
|
512
|
+
const lastOverlay = getPresentedOverlay(doc, OVERLAY_FOCUS_TRAP_SELECTOR);
|
|
513
|
+
if (!lastOverlay || lastOverlay.classList.contains(FOCUS_TRAP_DISABLE_CLASS)) {
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
const isSheetModal = lastOverlay.classList.contains('modal-sheet');
|
|
517
|
+
if (!isSheetModal) {
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
const target = ev.target;
|
|
521
|
+
if (!(target instanceof HTMLElement)) {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const optionHost = getOptionControlHost(target);
|
|
525
|
+
if (optionHost && lastOverlay.contains(optionHost)) {
|
|
526
|
+
lastOverlay.trapLastSheetOptionControl = optionHost;
|
|
527
|
+
}
|
|
528
|
+
}, true);
|
|
412
529
|
// Listen for keydown events to intercept Tab navigation.
|
|
413
530
|
// This is needed for Safari and Firefox which may skip focusable
|
|
414
531
|
// elements or allow focus to escape the overlay.
|
|
@@ -418,10 +535,10 @@ const connectListeners = (doc) => {
|
|
|
418
535
|
var _a, _b, _c;
|
|
419
536
|
if (ev.key !== 'Tab' && ev.key !== 'Alt+Tab')
|
|
420
537
|
return;
|
|
421
|
-
const lastOverlay = getPresentedOverlay(doc,
|
|
538
|
+
const lastOverlay = getPresentedOverlay(doc, OVERLAY_FOCUS_TRAP_SELECTOR);
|
|
422
539
|
if (!lastOverlay || lastOverlay.classList.contains(FOCUS_TRAP_DISABLE_CLASS))
|
|
423
540
|
return;
|
|
424
|
-
const activeElement = doc
|
|
541
|
+
const activeElement = getActiveElement(doc);
|
|
425
542
|
if (activeElement === lastOverlay) {
|
|
426
543
|
ev.preventDefault();
|
|
427
544
|
focusFirstDescendant(lastOverlay);
|
|
@@ -437,16 +554,32 @@ const connectListeners = (doc) => {
|
|
|
437
554
|
if (!isInsideOverlay)
|
|
438
555
|
return;
|
|
439
556
|
// Get all focusable elements from both light and shadow DOM
|
|
440
|
-
|
|
557
|
+
let allFocusable = [
|
|
441
558
|
...lastOverlay.querySelectorAll(focusableQueryString),
|
|
442
559
|
...(((_c = lastOverlay.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelectorAll(focusableQueryString)) || []),
|
|
443
560
|
];
|
|
561
|
+
const selectModalEl = lastOverlay.querySelector(ION_SELECT_MODAL_SELECTOR);
|
|
562
|
+
const isSheetModal = lastOverlay.classList.contains('modal-sheet');
|
|
563
|
+
/**
|
|
564
|
+
* Some sheet modal content, including ion-select-modal,
|
|
565
|
+
* renders option containers as `ion-item.select-interface-option`.
|
|
566
|
+
* These can match focusable selectors in some builds but
|
|
567
|
+
* are not intended tab stops. Keep only true interactive
|
|
568
|
+
* controls so Tab can move from options to header
|
|
569
|
+
* controls/handle.
|
|
570
|
+
*/
|
|
571
|
+
if (selectModalEl) {
|
|
572
|
+
allFocusable = allFocusable.filter((el) => !el.matches('ion-item.select-interface-option'));
|
|
573
|
+
}
|
|
574
|
+
if (isSheetModal) {
|
|
575
|
+
allFocusable = sortSheetModalFocusables(lastOverlay, allFocusable);
|
|
576
|
+
}
|
|
444
577
|
if (allFocusable.length === 0) {
|
|
445
578
|
ev.preventDefault();
|
|
446
579
|
return;
|
|
447
580
|
}
|
|
448
581
|
// Find current element's index (accounting for shadow DOM)
|
|
449
|
-
|
|
582
|
+
let currentIndex = activeElement
|
|
450
583
|
? allFocusable.findIndex((el) => {
|
|
451
584
|
var _a;
|
|
452
585
|
if (el === activeElement)
|
|
@@ -457,6 +590,31 @@ const connectListeners = (doc) => {
|
|
|
457
590
|
return rootNode instanceof ShadowRoot && rootNode.host === el;
|
|
458
591
|
})
|
|
459
592
|
: -1;
|
|
593
|
+
/**
|
|
594
|
+
* Radio/checkbox groups can move focus onto an option with
|
|
595
|
+
* `tabIndex=-1`, while another option still has `tabIndex=0`
|
|
596
|
+
* in the list. `findIndex` then yields -1 and Tab incorrectly
|
|
597
|
+
* wraps to `allFocusable[0]`.
|
|
598
|
+
* Treat focus on any option control inside the sheet modal
|
|
599
|
+
* as the same trap slot as the listed tabbable option
|
|
600
|
+
* (`tabIndex >= 0`) so Tab goes handle → Cancel, not back
|
|
601
|
+
* to the first radio.
|
|
602
|
+
*/
|
|
603
|
+
if (currentIndex < 0 && isSheetModal && activeElement) {
|
|
604
|
+
const optionHost = getOptionControlHost(activeElement);
|
|
605
|
+
if (optionHost && lastOverlay.contains(optionHost)) {
|
|
606
|
+
const directIndex = allFocusable.indexOf(optionHost);
|
|
607
|
+
if (directIndex >= 0) {
|
|
608
|
+
currentIndex = directIndex;
|
|
609
|
+
}
|
|
610
|
+
else {
|
|
611
|
+
const tabbableOptionIndex = getTabbableOptionControlIndex(allFocusable, lastOverlay);
|
|
612
|
+
if (tabbableOptionIndex >= 0) {
|
|
613
|
+
currentIndex = tabbableOptionIndex;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
460
618
|
ev.preventDefault();
|
|
461
619
|
// Helper to focus an element, handling shadow DOM properly
|
|
462
620
|
const focusElement = (element) => {
|
|
@@ -470,24 +628,43 @@ const connectListeners = (doc) => {
|
|
|
470
628
|
}
|
|
471
629
|
helpers.focusVisibleElement(element);
|
|
472
630
|
};
|
|
631
|
+
let nextIndex;
|
|
473
632
|
if (ev.shiftKey) {
|
|
474
633
|
// Shift+Tab: previous element, wrap to last if at first
|
|
475
634
|
if (currentIndex <= 0) {
|
|
476
|
-
|
|
635
|
+
nextIndex = allFocusable.length - 1;
|
|
477
636
|
}
|
|
478
637
|
else {
|
|
479
|
-
|
|
638
|
+
nextIndex = currentIndex - 1;
|
|
480
639
|
}
|
|
481
640
|
}
|
|
482
641
|
else {
|
|
483
642
|
// Tab: next element, wrap to first if at last
|
|
484
643
|
if (currentIndex < 0 || currentIndex >= allFocusable.length - 1) {
|
|
485
|
-
|
|
644
|
+
nextIndex = 0;
|
|
486
645
|
}
|
|
487
646
|
else {
|
|
488
|
-
|
|
647
|
+
nextIndex = currentIndex + 1;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
const nextEl = allFocusable[nextIndex];
|
|
651
|
+
const overlayTrap = lastOverlay;
|
|
652
|
+
const tabbableOptionIndex = isSheetModal ? getTabbableOptionControlIndex(allFocusable, lastOverlay) : -1;
|
|
653
|
+
/**
|
|
654
|
+
* The trap list only includes one tabbable option host
|
|
655
|
+
* (`tabIndex >= 0`), usually the checked/first radio.
|
|
656
|
+
* `focusin` tracks the real last-focused option; use it
|
|
657
|
+
* whenever Tab would focus that slot (wrap from Cancel,
|
|
658
|
+
* Shift+Tab from handle, etc.).
|
|
659
|
+
*/
|
|
660
|
+
let focusTarget = nextEl;
|
|
661
|
+
if (isSheetModal && tabbableOptionIndex >= 0 && nextIndex === tabbableOptionIndex) {
|
|
662
|
+
const saved = overlayTrap.trapLastSheetOptionControl;
|
|
663
|
+
if ((saved === null || saved === void 0 ? void 0 : saved.isConnected) && lastOverlay.contains(saved) && saved !== nextEl) {
|
|
664
|
+
focusTarget = saved;
|
|
489
665
|
}
|
|
490
666
|
}
|
|
667
|
+
focusElement(focusTarget);
|
|
491
668
|
}, true);
|
|
492
669
|
// handle back-button click
|
|
493
670
|
doc.addEventListener('ionBackButton', (ev) => {
|
|
@@ -25,7 +25,6 @@ const BREAKPOINT_ORDER = ['xs', 'sm', 'md', 'lg', 'xl', 'xxl'];
|
|
|
25
25
|
*/
|
|
26
26
|
export class Gallery {
|
|
27
27
|
constructor() {
|
|
28
|
-
this.itemWrapperSelector = '[data-gallery-group]';
|
|
29
28
|
// Keep track of whether we've warned about invalid columns, invalid gap,
|
|
30
29
|
// and unused order properties to avoid duplicate warnings on screen resize.
|
|
31
30
|
this.hasWarnedInvalidColumns = false;
|
|
@@ -80,9 +79,8 @@ export class Gallery {
|
|
|
80
79
|
const styles = getComputedStyle(this.el);
|
|
81
80
|
const rowHeight = parseFloat(styles.getPropertyValue('grid-auto-rows')) || 0;
|
|
82
81
|
const rowGap = parseFloat(styles.getPropertyValue('row-gap')) || parseFloat(styles.getPropertyValue('gap')) || 0;
|
|
83
|
-
const itemGap = parseFloat(styles.getPropertyValue('column-gap')) || parseFloat(styles.getPropertyValue('gap')) || 0;
|
|
84
82
|
const items = this.getItems();
|
|
85
|
-
this.layoutMasonry(items, rowHeight, rowGap,
|
|
83
|
+
this.layoutMasonry(items, rowHeight, rowGap, columns);
|
|
86
84
|
};
|
|
87
85
|
}
|
|
88
86
|
onColumnsOrGapChanged() {
|
|
@@ -339,28 +337,12 @@ export class Gallery {
|
|
|
339
337
|
const gap = this.getGapForWidth(width);
|
|
340
338
|
this.el.style.setProperty('--internal-gallery-gap', `${gap}`);
|
|
341
339
|
}
|
|
342
|
-
isGalleryItemElement(element) {
|
|
343
|
-
var _a;
|
|
344
|
-
return typeof ((_a = element.style) === null || _a === void 0 ? void 0 : _a.setProperty) === 'function';
|
|
345
|
-
}
|
|
346
340
|
/**
|
|
347
|
-
* Return all gallery
|
|
348
|
-
*
|
|
349
|
-
* with their element children.
|
|
341
|
+
* Return all directly slotted children of the gallery that can be grid items
|
|
342
|
+
* with inline placement styles (HTML elements and SVG elements).
|
|
350
343
|
*/
|
|
351
344
|
getItems() {
|
|
352
|
-
|
|
353
|
-
const flattenedItems = [];
|
|
354
|
-
items.forEach((itemEl) => {
|
|
355
|
-
if (!itemEl.matches(this.itemWrapperSelector)) {
|
|
356
|
-
flattenedItems.push(itemEl);
|
|
357
|
-
return;
|
|
358
|
-
}
|
|
359
|
-
itemEl.style.display = 'contents';
|
|
360
|
-
const wrappedItems = Array.from(itemEl.children).filter((child) => this.isGalleryItemElement(child));
|
|
361
|
-
flattenedItems.push(...wrappedItems);
|
|
362
|
-
});
|
|
363
|
-
return flattenedItems;
|
|
345
|
+
return Array.from(this.el.children).filter((child) => { var _a; return typeof ((_a = child.style) === null || _a === void 0 ? void 0 : _a.setProperty) === 'function'; });
|
|
364
346
|
}
|
|
365
347
|
/**
|
|
366
348
|
* Clear the item styles for the given item element.
|
|
@@ -417,14 +399,11 @@ export class Gallery {
|
|
|
417
399
|
/**
|
|
418
400
|
* Apply masonry placement by assigning each item a column and row span.
|
|
419
401
|
*/
|
|
420
|
-
layoutMasonry(items, rowHeight, rowGap,
|
|
402
|
+
layoutMasonry(items, rowHeight, rowGap, columns) {
|
|
421
403
|
const columnHeights = new Array(columns).fill(0);
|
|
422
404
|
const lastItemsByColumn = new Array(columns).fill(undefined);
|
|
423
405
|
items.forEach((itemEl, i) => {
|
|
424
406
|
itemEl.style.marginBottom = '';
|
|
425
|
-
if (itemEl.parentElement !== this.el) {
|
|
426
|
-
itemEl.style.marginBottom = `${itemGap}px`;
|
|
427
|
-
}
|
|
428
407
|
const span = this.calculateRowSpan(itemEl, rowHeight, rowGap);
|
|
429
408
|
if (span === undefined) {
|
|
430
409
|
this.clearItemStyles(itemEl);
|
|
@@ -473,11 +452,11 @@ export class Gallery {
|
|
|
473
452
|
const { layout } = this;
|
|
474
453
|
const order = this.getOrder();
|
|
475
454
|
const theme = getIonTheme(this);
|
|
476
|
-
return (h(Host, { key: '
|
|
455
|
+
return (h(Host, { key: '1bf2973d22835c0dbddf3214b602f8c08b95e421', class: {
|
|
477
456
|
[theme]: true,
|
|
478
457
|
[`gallery-layout-${layout}`]: true,
|
|
479
458
|
[`gallery-order-${order}`]: layout === 'masonry' && order !== undefined,
|
|
480
|
-
} }, h("slot", { key: '
|
|
459
|
+
} }, h("slot", { key: '0dea31f609f6afdb1d73ecb2d873909ffe49203f', onSlotchange: this.onSlotChange })));
|
|
481
460
|
}
|
|
482
461
|
static get is() { return "ion-gallery"; }
|
|
483
462
|
static get encapsulation() { return "shadow"; }
|
|
@@ -14,8 +14,101 @@ import { addEventListener, componentOnReady, focusVisibleElement, getElementRoot
|
|
|
14
14
|
let lastOverlayIndex = 0;
|
|
15
15
|
let lastId = 0;
|
|
16
16
|
export const activeAnimations = new WeakMap();
|
|
17
|
+
const OVERLAY_FOCUS_TRAP_SELECTOR = 'ion-alert,ion-action-sheet,ion-loading,ion-modal,ion-picker-legacy,ion-popover';
|
|
18
|
+
const ION_SELECT_MODAL_SELECTOR = 'ion-select-modal';
|
|
19
|
+
const isSelectModalOptionControl = (el) => el.tagName === 'ION-RADIO' || el.tagName === 'ION-CHECKBOX';
|
|
17
20
|
/**
|
|
18
|
-
*
|
|
21
|
+
* Returns the currently focused element for keyboard focus checks.
|
|
22
|
+
*
|
|
23
|
+
* Starts from `document.activeElement` (non-shadow / light DOM focus).
|
|
24
|
+
* If focus is inside one or more open shadow roots
|
|
25
|
+
* (e.g. native control inside `ion-radio`), walks through nested
|
|
26
|
+
* `shadowRoot.activeElement` values until the innermost focused node is reached.
|
|
27
|
+
*/
|
|
28
|
+
const getActiveElement = (ownerDoc) => {
|
|
29
|
+
var _a;
|
|
30
|
+
let active = ownerDoc.activeElement;
|
|
31
|
+
if (!active) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
while ((_a = active.shadowRoot) === null || _a === void 0 ? void 0 : _a.activeElement) {
|
|
35
|
+
active = active.shadowRoot.activeElement;
|
|
36
|
+
}
|
|
37
|
+
return active;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Walks from a focused node (possibly deep inside shadow roots)
|
|
41
|
+
* up to the nearest `ion-radio` / `ion-checkbox` host.
|
|
42
|
+
*/
|
|
43
|
+
const getOptionControlHost = (active) => {
|
|
44
|
+
let n = active;
|
|
45
|
+
while (n) {
|
|
46
|
+
if (isSelectModalOptionControl(n)) {
|
|
47
|
+
return n;
|
|
48
|
+
}
|
|
49
|
+
const root = n.getRootNode();
|
|
50
|
+
if (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
|
|
51
|
+
n = root.host;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return null;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Sheet modals can have visual order that differs from DOM order.
|
|
61
|
+
* Without sorting, Tab can skip the handle after option traversal.
|
|
62
|
+
*
|
|
63
|
+
* Order: option controls (radio/checkbox) → sheet handle → end-slot
|
|
64
|
+
* header button. Any other focusables are appended after.
|
|
65
|
+
*/
|
|
66
|
+
const sortSheetModalFocusables = (overlay, elements) => {
|
|
67
|
+
const optionControls = elements.filter((el) => {
|
|
68
|
+
return overlay.contains(el) && isSelectModalOptionControl(el) && el.tabIndex >= 0;
|
|
69
|
+
});
|
|
70
|
+
const cancelControl = elements.find((el) => overlay.contains(el) &&
|
|
71
|
+
el.tagName === 'ION-BUTTON' &&
|
|
72
|
+
el.closest('ion-header ion-buttons[slot="end"]') !== null);
|
|
73
|
+
const handleControl = elements.find((el) => el.classList.contains('modal-handle'));
|
|
74
|
+
const sortByGeometry = (els) => [...els].sort((a, b) => {
|
|
75
|
+
const ra = a.getBoundingClientRect();
|
|
76
|
+
const rb = b.getBoundingClientRect();
|
|
77
|
+
const topDiff = ra.top - rb.top;
|
|
78
|
+
if (Math.abs(topDiff) > 1) {
|
|
79
|
+
return topDiff;
|
|
80
|
+
}
|
|
81
|
+
return ra.left - rb.left;
|
|
82
|
+
});
|
|
83
|
+
const ordered = [];
|
|
84
|
+
ordered.push(...sortByGeometry(optionControls));
|
|
85
|
+
if (handleControl) {
|
|
86
|
+
ordered.push(handleControl);
|
|
87
|
+
}
|
|
88
|
+
if (cancelControl) {
|
|
89
|
+
ordered.push(cancelControl);
|
|
90
|
+
}
|
|
91
|
+
const used = new Set(ordered);
|
|
92
|
+
for (const el of elements) {
|
|
93
|
+
if (!used.has(el)) {
|
|
94
|
+
ordered.push(el);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return ordered;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Option controls in groups use a tabindex pattern where only one
|
|
101
|
+
* option is tabbable (`tabIndex="0"`) while the others are `-1`.
|
|
102
|
+
* This returns the index of that current tabbable option.
|
|
103
|
+
*/
|
|
104
|
+
const getTabbableOptionControlIndex = (elements, overlay) => {
|
|
105
|
+
return elements.findIndex((el) => {
|
|
106
|
+
return overlay.contains(el) && isSelectModalOptionControl(el) && el.tabIndex >= 0;
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Determines if the overlay's backdrop is always blocking
|
|
111
|
+
* (no background interaction).
|
|
19
112
|
* Returns false if showBackdrop=false or backdropBreakpoint > 0.
|
|
20
113
|
*/
|
|
21
114
|
const isBackdropAlwaysBlocking = (el) => {
|
|
@@ -136,7 +229,7 @@ const focusElementInOverlay = (hostToFocus, overlay) => {
|
|
|
136
229
|
* Should NOT include: Toast
|
|
137
230
|
*/
|
|
138
231
|
const trapKeyboardFocus = (ev, doc) => {
|
|
139
|
-
const lastOverlay = getPresentedOverlay(doc,
|
|
232
|
+
const lastOverlay = getPresentedOverlay(doc, OVERLAY_FOCUS_TRAP_SELECTOR);
|
|
140
233
|
const target = ev.target;
|
|
141
234
|
/**
|
|
142
235
|
* If no active overlay, ignore this event.
|
|
@@ -319,6 +412,30 @@ const connectListeners = (doc) => {
|
|
|
319
412
|
doc.addEventListener('focus', (ev) => {
|
|
320
413
|
trapKeyboardFocus(ev, doc);
|
|
321
414
|
}, true);
|
|
415
|
+
/**
|
|
416
|
+
* Remember which option control last received focus
|
|
417
|
+
* (arrows, click, or Tab). This pattern keeps `tabIndex=0` on the
|
|
418
|
+
* checked/first radio, so the Tab trap uses this when wrapping back
|
|
419
|
+
* into the list or focusing the option-group slot.
|
|
420
|
+
*/
|
|
421
|
+
doc.addEventListener('focusin', (ev) => {
|
|
422
|
+
const lastOverlay = getPresentedOverlay(doc, OVERLAY_FOCUS_TRAP_SELECTOR);
|
|
423
|
+
if (!lastOverlay || lastOverlay.classList.contains(FOCUS_TRAP_DISABLE_CLASS)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
const isSheetModal = lastOverlay.classList.contains('modal-sheet');
|
|
427
|
+
if (!isSheetModal) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const target = ev.target;
|
|
431
|
+
if (!(target instanceof HTMLElement)) {
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
const optionHost = getOptionControlHost(target);
|
|
435
|
+
if (optionHost && lastOverlay.contains(optionHost)) {
|
|
436
|
+
lastOverlay.trapLastSheetOptionControl = optionHost;
|
|
437
|
+
}
|
|
438
|
+
}, true);
|
|
322
439
|
// Listen for keydown events to intercept Tab navigation.
|
|
323
440
|
// This is needed for Safari and Firefox which may skip focusable
|
|
324
441
|
// elements or allow focus to escape the overlay.
|
|
@@ -328,10 +445,10 @@ const connectListeners = (doc) => {
|
|
|
328
445
|
var _a, _b, _c;
|
|
329
446
|
if (ev.key !== 'Tab' && ev.key !== 'Alt+Tab')
|
|
330
447
|
return;
|
|
331
|
-
const lastOverlay = getPresentedOverlay(doc,
|
|
448
|
+
const lastOverlay = getPresentedOverlay(doc, OVERLAY_FOCUS_TRAP_SELECTOR);
|
|
332
449
|
if (!lastOverlay || lastOverlay.classList.contains(FOCUS_TRAP_DISABLE_CLASS))
|
|
333
450
|
return;
|
|
334
|
-
const activeElement = doc
|
|
451
|
+
const activeElement = getActiveElement(doc);
|
|
335
452
|
if (activeElement === lastOverlay) {
|
|
336
453
|
ev.preventDefault();
|
|
337
454
|
focusFirstDescendant(lastOverlay);
|
|
@@ -347,16 +464,32 @@ const connectListeners = (doc) => {
|
|
|
347
464
|
if (!isInsideOverlay)
|
|
348
465
|
return;
|
|
349
466
|
// Get all focusable elements from both light and shadow DOM
|
|
350
|
-
|
|
467
|
+
let allFocusable = [
|
|
351
468
|
...lastOverlay.querySelectorAll(focusableQueryString),
|
|
352
469
|
...(((_c = lastOverlay.shadowRoot) === null || _c === void 0 ? void 0 : _c.querySelectorAll(focusableQueryString)) || []),
|
|
353
470
|
];
|
|
471
|
+
const selectModalEl = lastOverlay.querySelector(ION_SELECT_MODAL_SELECTOR);
|
|
472
|
+
const isSheetModal = lastOverlay.classList.contains('modal-sheet');
|
|
473
|
+
/**
|
|
474
|
+
* Some sheet modal content, including ion-select-modal,
|
|
475
|
+
* renders option containers as `ion-item.select-interface-option`.
|
|
476
|
+
* These can match focusable selectors in some builds but
|
|
477
|
+
* are not intended tab stops. Keep only true interactive
|
|
478
|
+
* controls so Tab can move from options to header
|
|
479
|
+
* controls/handle.
|
|
480
|
+
*/
|
|
481
|
+
if (selectModalEl) {
|
|
482
|
+
allFocusable = allFocusable.filter((el) => !el.matches('ion-item.select-interface-option'));
|
|
483
|
+
}
|
|
484
|
+
if (isSheetModal) {
|
|
485
|
+
allFocusable = sortSheetModalFocusables(lastOverlay, allFocusable);
|
|
486
|
+
}
|
|
354
487
|
if (allFocusable.length === 0) {
|
|
355
488
|
ev.preventDefault();
|
|
356
489
|
return;
|
|
357
490
|
}
|
|
358
491
|
// Find current element's index (accounting for shadow DOM)
|
|
359
|
-
|
|
492
|
+
let currentIndex = activeElement
|
|
360
493
|
? allFocusable.findIndex((el) => {
|
|
361
494
|
var _a;
|
|
362
495
|
if (el === activeElement)
|
|
@@ -367,6 +500,31 @@ const connectListeners = (doc) => {
|
|
|
367
500
|
return rootNode instanceof ShadowRoot && rootNode.host === el;
|
|
368
501
|
})
|
|
369
502
|
: -1;
|
|
503
|
+
/**
|
|
504
|
+
* Radio/checkbox groups can move focus onto an option with
|
|
505
|
+
* `tabIndex=-1`, while another option still has `tabIndex=0`
|
|
506
|
+
* in the list. `findIndex` then yields -1 and Tab incorrectly
|
|
507
|
+
* wraps to `allFocusable[0]`.
|
|
508
|
+
* Treat focus on any option control inside the sheet modal
|
|
509
|
+
* as the same trap slot as the listed tabbable option
|
|
510
|
+
* (`tabIndex >= 0`) so Tab goes handle → Cancel, not back
|
|
511
|
+
* to the first radio.
|
|
512
|
+
*/
|
|
513
|
+
if (currentIndex < 0 && isSheetModal && activeElement) {
|
|
514
|
+
const optionHost = getOptionControlHost(activeElement);
|
|
515
|
+
if (optionHost && lastOverlay.contains(optionHost)) {
|
|
516
|
+
const directIndex = allFocusable.indexOf(optionHost);
|
|
517
|
+
if (directIndex >= 0) {
|
|
518
|
+
currentIndex = directIndex;
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
const tabbableOptionIndex = getTabbableOptionControlIndex(allFocusable, lastOverlay);
|
|
522
|
+
if (tabbableOptionIndex >= 0) {
|
|
523
|
+
currentIndex = tabbableOptionIndex;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
370
528
|
ev.preventDefault();
|
|
371
529
|
// Helper to focus an element, handling shadow DOM properly
|
|
372
530
|
const focusElement = (element) => {
|
|
@@ -380,24 +538,43 @@ const connectListeners = (doc) => {
|
|
|
380
538
|
}
|
|
381
539
|
focusVisibleElement(element);
|
|
382
540
|
};
|
|
541
|
+
let nextIndex;
|
|
383
542
|
if (ev.shiftKey) {
|
|
384
543
|
// Shift+Tab: previous element, wrap to last if at first
|
|
385
544
|
if (currentIndex <= 0) {
|
|
386
|
-
|
|
545
|
+
nextIndex = allFocusable.length - 1;
|
|
387
546
|
}
|
|
388
547
|
else {
|
|
389
|
-
|
|
548
|
+
nextIndex = currentIndex - 1;
|
|
390
549
|
}
|
|
391
550
|
}
|
|
392
551
|
else {
|
|
393
552
|
// Tab: next element, wrap to first if at last
|
|
394
553
|
if (currentIndex < 0 || currentIndex >= allFocusable.length - 1) {
|
|
395
|
-
|
|
554
|
+
nextIndex = 0;
|
|
396
555
|
}
|
|
397
556
|
else {
|
|
398
|
-
|
|
557
|
+
nextIndex = currentIndex + 1;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
const nextEl = allFocusable[nextIndex];
|
|
561
|
+
const overlayTrap = lastOverlay;
|
|
562
|
+
const tabbableOptionIndex = isSheetModal ? getTabbableOptionControlIndex(allFocusable, lastOverlay) : -1;
|
|
563
|
+
/**
|
|
564
|
+
* The trap list only includes one tabbable option host
|
|
565
|
+
* (`tabIndex >= 0`), usually the checked/first radio.
|
|
566
|
+
* `focusin` tracks the real last-focused option; use it
|
|
567
|
+
* whenever Tab would focus that slot (wrap from Cancel,
|
|
568
|
+
* Shift+Tab from handle, etc.).
|
|
569
|
+
*/
|
|
570
|
+
let focusTarget = nextEl;
|
|
571
|
+
if (isSheetModal && tabbableOptionIndex >= 0 && nextIndex === tabbableOptionIndex) {
|
|
572
|
+
const saved = overlayTrap.trapLastSheetOptionControl;
|
|
573
|
+
if ((saved === null || saved === void 0 ? void 0 : saved.isConnected) && lastOverlay.contains(saved) && saved !== nextEl) {
|
|
574
|
+
focusTarget = saved;
|
|
399
575
|
}
|
|
400
576
|
}
|
|
577
|
+
focusElement(focusTarget);
|
|
401
578
|
}, true);
|
|
402
579
|
// handle back-button click
|
|
403
580
|
doc.addEventListener('ionBackButton', (ev) => {
|
package/dist/docs.json
CHANGED
package/dist/esm/index.js
CHANGED
|
@@ -14,7 +14,7 @@ export { I as IonicSafeString } from './index-D4ugF_sT.js';
|
|
|
14
14
|
export { g as getMode, s as setupConfig } from './config-BwKpO3Is.js';
|
|
15
15
|
export { o as openURL } from './theme-DaJxRxSQ.js';
|
|
16
16
|
export { m as menuController } from './index-hpH08p5s.js';
|
|
17
|
-
export { b as actionSheetController, a as alertController, l as loadingController, m as modalController, p as pickerController, c as popoverController, t as toastController } from './overlays-
|
|
17
|
+
export { b as actionSheetController, a as alertController, l as loadingController, m as modalController, p as pickerController, c as popoverController, t as toastController } from './overlays-ttYCMKRp.js';
|
|
18
18
|
import './gesture-controller-BTEOs1at.js';
|
|
19
19
|
import './focus-visible-vXpMhGrs.js';
|
|
20
20
|
import './framework-delegate-CjVwn_KZ.js';
|
|
@@ -5,7 +5,7 @@ import { r as registerInstance, c as createEvent, a as readTask, h, d as Host, g
|
|
|
5
5
|
import { c as createButtonActiveGesture } from './button-active-g6ZnZzDZ.js';
|
|
6
6
|
import { r as raf } from './helpers-Do7zwvM1.js';
|
|
7
7
|
import { c as createLockController } from './lock-controller-B-hirT0v.js';
|
|
8
|
-
import { d as createDelegateController, e as createTriggerController, B as BACKDROP, i as isCancel, f as present, g as dismiss, h as eventMethod, s as safeCall, j as prepareOverlay, k as setOverlayId } from './overlays-
|
|
8
|
+
import { d as createDelegateController, e as createTriggerController, B as BACKDROP, i as isCancel, f as present, g as dismiss, h as eventMethod, s as safeCall, j as prepareOverlay, k as setOverlayId } from './overlays-ttYCMKRp.js';
|
|
9
9
|
import { r as renderOptionLabel } from './select-option-render-B2qc5ZP7.js';
|
|
10
10
|
import { g as getClassMap } from './theme-DaJxRxSQ.js';
|
|
11
11
|
import { b as getIonMode, c as getIonTheme } from './ionic-global-CAZb-5i-.js';
|
|
@@ -6,7 +6,7 @@ import { E as ENABLE_HTML_CONTENT_DEFAULT } from './config-BwKpO3Is.js';
|
|
|
6
6
|
import { c as createButtonActiveGesture } from './button-active-g6ZnZzDZ.js';
|
|
7
7
|
import { r as raf } from './helpers-Do7zwvM1.js';
|
|
8
8
|
import { c as createLockController } from './lock-controller-B-hirT0v.js';
|
|
9
|
-
import { d as createDelegateController, e as createTriggerController, B as BACKDROP, i as isCancel, j as prepareOverlay, k as setOverlayId, f as present, g as dismiss, h as eventMethod, s as safeCall } from './overlays-
|
|
9
|
+
import { d as createDelegateController, e as createTriggerController, B as BACKDROP, i as isCancel, j as prepareOverlay, k as setOverlayId, f as present, g as dismiss, h as eventMethod, s as safeCall } from './overlays-ttYCMKRp.js';
|
|
10
10
|
import { s as sanitizeDOMString } from './index-D4ugF_sT.js';
|
|
11
11
|
import { r as renderOptionLabel } from './select-option-render-B2qc5ZP7.js';
|
|
12
12
|
import { g as getClassMap } from './theme-DaJxRxSQ.js';
|
|
@@ -6,7 +6,7 @@ import { c as caretLeftSvg } from './caret-left-fIOYmaqA.js';
|
|
|
6
6
|
import { c as caretRightSvg } from './caret-right-BYSs-jZz.js';
|
|
7
7
|
import { startFocusVisible } from './focus-visible-vXpMhGrs.js';
|
|
8
8
|
import { r as raf, g as getElementRoot, a as renderHiddenInput, e as clamp } from './helpers-Do7zwvM1.js';
|
|
9
|
-
import { F as FOCUS_TRAP_DISABLE_CLASS, d as createDelegateController, e as createTriggerController, B as BACKDROP, i as isCancel, j as prepareOverlay, k as setOverlayId, f as present, g as dismiss, h as eventMethod, s as safeCall } from './overlays-
|
|
9
|
+
import { F as FOCUS_TRAP_DISABLE_CLASS, d as createDelegateController, e as createTriggerController, B as BACKDROP, i as isCancel, j as prepareOverlay, k as setOverlayId, f as present, g as dismiss, h as eventMethod, s as safeCall } from './overlays-ttYCMKRp.js';
|
|
10
10
|
import { i as isRTL } from './dir-C53feagD.js';
|
|
11
11
|
import { c as createColorClasses, g as getClassMap } from './theme-DaJxRxSQ.js';
|
|
12
12
|
import { o as chevronForward, c as chevronBack, p as caretDownSharp, q as caretUpSharp, l as chevronDown } from './index-D2tu5BUg.js';
|