Skip to content

Commit 1484347

Browse files
committed
fix: exclude toolbar elements from tab-order & mark overflow-clipped focusables as unreachable
1 parent 1d224e6 commit 1484347

2 files changed

Lines changed: 49 additions & 3 deletions

File tree

src/view/frontend/web/css/audits/tab-order.css

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,3 +58,12 @@
5858
.mageforge-tab-order-badge--negative {
5959
background: var(--mageforge-color-red);
6060
}
61+
62+
/* Element is focusable but clipped by an overflow:hidden ancestor (e.g. off-canvas slider item) */
63+
.mageforge-tab-order-badge--clipped {
64+
background: var(--mageforge-color-orange);
65+
}
66+
67+
.mageforge-tab-order-line--clipped {
68+
stroke: var(--mageforge-color-orange);
69+
}

src/view/frontend/web/js/toolbar/audits/tab-order.js

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,34 @@ function isVisible(el) {
4040
return style.visibility !== 'hidden' && style.display !== 'none';
4141
}
4242

43+
/**
44+
* Returns true if the element lies completely outside the visible area of an
45+
* ancestor with overflow:hidden/clip (e.g. a carousel slide that is off-canvas).
46+
*/
47+
function isClippedByAncestor(el) {
48+
const elRect = el.getBoundingClientRect();
49+
let ancestor = el.parentElement;
50+
while (ancestor && ancestor !== document.documentElement) {
51+
const style = getComputedStyle(ancestor);
52+
const clipsX = ['hidden', 'clip', 'scroll', 'auto'].includes(style.overflowX);
53+
const clipsY = ['hidden', 'clip', 'scroll', 'auto'].includes(style.overflowY);
54+
if (clipsX || clipsY) {
55+
const aRect = ancestor.getBoundingClientRect();
56+
// No intersection at all → element is fully clipped away
57+
if (
58+
elRect.right <= aRect.left ||
59+
elRect.left >= aRect.right ||
60+
elRect.bottom <= aRect.top ||
61+
elRect.top >= aRect.bottom
62+
) {
63+
return true;
64+
}
65+
}
66+
ancestor = ancestor.parentElement;
67+
}
68+
return false;
69+
}
70+
4371
function getTabIndex(el) {
4472
const value = parseInt(el.getAttribute('tabindex'), 10);
4573
return isNaN(value) ? 0 : value;
@@ -95,15 +123,17 @@ function renderOverlay(sorted) {
95123
const cx = Math.round(rect.left + rect.width / 2);
96124
const cy = Math.round(rect.top);
97125

126+
const clipped = isClippedByAncestor(el);
98127
const badge = document.createElement('span');
99128
badge.className = 'mageforge-tab-order-badge' +
100-
(getTabIndex(el) > 0 ? ' mageforge-tab-order-badge--negative' : '');
129+
(getTabIndex(el) > 0 ? ' mageforge-tab-order-badge--negative' : '') +
130+
(clipped ? ' mageforge-tab-order-badge--clipped' : '');
101131
badge.textContent = index + 1;
102132
badge.style.left = cx + 'px';
103133
badge.style.top = cy + 'px';
104134
overlay.appendChild(badge);
105135

106-
return { cx, cy, negative: getTabIndex(el) > 0 };
136+
return { cx, cy, negative: getTabIndex(el) > 0, clipped };
107137
});
108138

109139
// Draw connecting lines between consecutive badges
@@ -114,6 +144,8 @@ function renderOverlay(sorted) {
114144
line.classList.add('mageforge-tab-order-line');
115145
if (from.negative || to.negative) {
116146
line.classList.add('mageforge-tab-order-line--negative');
147+
} else if (from.clipped || to.clipped) {
148+
line.classList.add('mageforge-tab-order-line--clipped');
117149
}
118150
line.setAttribute('x1', from.cx);
119151
line.setAttribute('y1', from.cy);
@@ -149,6 +181,7 @@ export default {
149181
}
150182

151183
const allFocusable = Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR))
184+
.filter(el => !el.closest('.mageforge-toolbar'))
152185
.filter(isVisible);
153186

154187
if (allFocusable.length === 0) {
@@ -163,7 +196,11 @@ export default {
163196

164197
// Always recompute from the live DOM so detached / newly added elements are handled correctly
165198
const rerender = () => renderOverlay(
166-
sortByTabOrder(Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR)).filter(isVisible))
199+
sortByTabOrder(
200+
Array.from(document.querySelectorAll(FOCUSABLE_SELECTOR))
201+
.filter(el => !el.closest('.mageforge-toolbar'))
202+
.filter(isVisible)
203+
)
167204
);
168205

169206
// Re-render on resize or scroll (e.g. DevTools panel, page scroll)

0 commit comments

Comments
 (0)