@@ -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+
4371function 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