@@ -202,4 +202,104 @@ describe('reorder elements', () => {
202202 expect ( scrollTarget . scrollTop ) . to . equal ( scrollTop ) ;
203203 } ) ;
204204 } ) ;
205+
206+ // Regression tests for vaadin/web-components#11639. When the virtualizer
207+ // lives inside a shadow root, the focused row must be located via the
208+ // shadow root's `activeElement` (focus inside the shadow tree) or by
209+ // walking the flattened ancestors via `assignedSlot` from the scroll
210+ // target's root activeElement (focus on slotted light-DOM content).
211+ describe ( 'focused element detection in shadow tree' , ( ) => {
212+ beforeEach ( ( ) => {
213+ // Remove default scroll target
214+ scrollTarget . remove ( ) ;
215+ clock . restore ( ) ;
216+
217+ scrollTarget = fixtureSync ( '<div style="height: 100px;"></div>' ) ;
218+ const shadowRoot = scrollTarget . attachShadow ( { mode : 'open' } ) ;
219+ shadowRoot . innerHTML = `
220+ <style>:host { display: block; height: 100%; }</style>
221+ <div id="scrollTarget" style="height: 100%; overflow: auto;">
222+ <div id="container"></div>
223+ </div>
224+ ` ;
225+
226+ virtualizer = new Virtualizer ( {
227+ createElements : ( count ) =>
228+ Array . from ( Array ( count ) ) . map ( ( ) => {
229+ const row = document . createElement ( 'div' ) ;
230+ const slot = document . createElement ( 'slot' ) ;
231+ slot . name = `cell-${ scrollTarget . children . length } ` ;
232+ row . appendChild ( slot ) ;
233+
234+ const cellContent = document . createElement ( 'div' ) ;
235+ cellContent . slot = slot . name ;
236+ cellContent . tabIndex = 0 ;
237+ scrollTarget . appendChild ( cellContent ) ;
238+
239+ return row ;
240+ } ) ,
241+ updateElement : ( row , index ) => {
242+ row . index = index ;
243+ row . id = `row-${ index } ` ;
244+ const cellContent = row . querySelector ( 'slot' ) . assignedNodes ( ) [ 0 ] ;
245+ cellContent . textContent = index ;
246+ } ,
247+ scrollTarget : shadowRoot . getElementById ( 'scrollTarget' ) ,
248+ scrollContainer : shadowRoot . getElementById ( 'container' ) ,
249+ reorderElements : true ,
250+ } ) ;
251+ virtualizer . size = 100 ;
252+ elementsContainer = shadowRoot . getElementById ( 'container' ) ;
253+ } ) ;
254+
255+ it ( 'should not detach a focused row on reorder' , async ( ) => {
256+ // The row that will receive focus (should not get detached)
257+ const rowContainingFocus = elementsContainer . children [ 4 ] ;
258+ // Focus the row directly. The row lives inside the shadow tree, so
259+ // `document.activeElement` resolves to the shadow host, not the row.
260+ rowContainingFocus . tabIndex = 0 ;
261+ rowContainingFocus . focus ( ) ;
262+
263+ // Scroll and collect the detached elements
264+ const detachedElements = await scrollRecycle ( ) ;
265+ // Expect the focused row to not have been detached
266+ expect ( detachedElements ) . not . to . include ( rowContainingFocus ) ;
267+ } ) ;
268+
269+ it ( 'should not detach a row whose slotted content has focus on reorder' , async ( ) => {
270+ // The row that will include the focused cell (should not get detached)
271+ const rowContainingFocus = elementsContainer . children [ 4 ] ;
272+ // Focus the slotted cell content on the row
273+ rowContainingFocus . querySelector ( 'slot' ) . assignedNodes ( ) [ 0 ] . focus ( ) ;
274+
275+ // Scroll and collect the detached elements
276+ const detachedElements = await scrollRecycle ( ) ;
277+ // Expect the row containing focus to not have been detached
278+ expect ( detachedElements ) . not . to . include ( rowContainingFocus ) ;
279+ } ) ;
280+
281+ // When the entire virtualizer host is itself nested inside another
282+ // shadow root, `document.activeElement` retargets to the outermost
283+ // host. The scroll target's root activeElement remains the actual
284+ // slotted focused element and must be used as the walk's starting
285+ // point.
286+ it ( 'should not detach a row whose slotted content has focus when wrapped in an outer shadow' , async ( ) => {
287+ // Move the virtualizer host into an outer shadow root, keeping the
288+ // slotted cell contents (currently in `scrollTarget`'s light DOM)
289+ // wrapped inside the outer shadow as well.
290+ const outerHost = fixtureSync ( '<div></div>' ) ;
291+ const outerShadow = outerHost . attachShadow ( { mode : 'open' } ) ;
292+ outerShadow . appendChild ( scrollTarget ) ;
293+
294+ // The row that will include the focused cell (should not get detached)
295+ const rowContainingFocus = elementsContainer . children [ 4 ] ;
296+ // Focus the slotted cell content on the row
297+ rowContainingFocus . querySelector ( 'slot' ) . assignedNodes ( ) [ 0 ] . focus ( ) ;
298+
299+ // Scroll and collect the detached elements
300+ const detachedElements = await scrollRecycle ( ) ;
301+ // Expect the row containing focus to not have been detached
302+ expect ( detachedElements ) . not . to . include ( rowContainingFocus ) ;
303+ } ) ;
304+ } ) ;
205305} ) ;
0 commit comments