diff --git a/packages/tags/test/tags.test.ts b/packages/tags/test/tags.test.ts index d59f005968..86eea666f4 100644 --- a/packages/tags/test/tags.test.ts +++ b/packages/tags/test/tags.test.ts @@ -28,7 +28,8 @@ import { pageUpEvent, testForLitDevWarnings, } from '../../../test/testing-helpers.js'; -import { executeServerCommand } from '@web/test-runner-commands'; +import { sendKeys } from '@web/test-runner-commands'; +import { nextFrame } from '@spectrum-web-components/overlay/src/AbstractOverlay.js'; describe('Tags', () => { testForLitDevWarnings( @@ -176,6 +177,74 @@ describe('Tags', () => { tag1.blur(); }); + + it('handles focus when Tag is deleted', async () => { + const el = await fixture( + html` + + Tag 1 + Tag 2 + Tag 3 + Tag 4 + Tag 5 + + ` + ); + + await elementUpdated(el); + + const tag1 = el.querySelector('sp-tag#t1') as Tag; + const tag2 = el.querySelector('sp-tag#t2') as Tag; + const tag3 = el.querySelector('sp-tag#t3') as Tag; + const tag4 = el.querySelector('sp-tag#t4') as Tag; + const tag5 = el.querySelector('sp-tag#t5') as Tag; + + tag1.focus(); + await elementUpdated(el); + + await sendKeys({ + press: 'ArrowRight', + }); + await elementUpdated(el); + await nextFrame(); + await nextFrame(); + + expect(document.activeElement === tag2).to.be.true; + + await sendKeys({ + press: 'Delete', + }); + await elementUpdated(el); + await nextFrame(); + await nextFrame(); + + expect(document.activeElement === tag3).to.be.true; + + await sendKeys({ + press: 'ArrowRight', + }); + await elementUpdated(el); + await nextFrame(); + await nextFrame(); + await sendKeys({ + press: 'ArrowRight', + }); + await elementUpdated(el); + await nextFrame(); + await nextFrame(); + + expect(document.activeElement === tag5).to.be.true; + + await sendKeys({ + press: 'Delete', + }); + await elementUpdated(el); + await nextFrame(); + await nextFrame(); + + expect(document.activeElement === tag4).to.be.true; + }); + it('will not focus [disabled] children', async () => { const el = await fixture( html` @@ -288,12 +357,12 @@ describe('Tags', () => { const tagA = tagset2.querySelector('sp-tag:nth-child(1)') as Tag; const tagB = tagset2.querySelector('sp-tag:nth-child(2)') as Tag; - await executeServerCommand('send-keys', { + await sendKeys({ press: 'Tab', }); expect(document.activeElement === tag1).to.be.true; - await executeServerCommand('send-keys', { + await sendKeys({ press: 'Tab', }); expect(document.activeElement === tagA).to.be.true; diff --git a/tools/reactive-controllers/src/FocusGroup.ts b/tools/reactive-controllers/src/FocusGroup.ts index 115171bcd0..744012044c 100644 --- a/tools/reactive-controllers/src/FocusGroup.ts +++ b/tools/reactive-controllers/src/FocusGroup.ts @@ -10,7 +10,6 @@ OF ANY KIND, either express or implied. See the License for the specific languag governing permissions and limitations under the License. */ import type { ReactiveController, ReactiveElement } from 'lit'; -import { MutationController } from '@lit-labs/observers/mutation-controller.js'; type DirectionTypes = 'horizontal' | 'vertical' | 'both' | 'grid'; export type FocusGroupConfig = { @@ -39,6 +38,7 @@ export class FocusGroupController implements ReactiveController { protected cachedElements?: T[]; + private mutationObserver: MutationObserver; get currentIndex(): number { if (this._currentIndex === -1) { @@ -113,6 +113,8 @@ export class FocusGroupController // and the first rendered element. offset = 0; + recentlyConnected = false; + constructor( host: ReactiveElement, { @@ -124,14 +126,8 @@ export class FocusGroupController listenerScope, }: FocusGroupConfig = { elements: () => [] } ) { - new MutationController(host, { - config: { - childList: true, - subtree: true, - }, - callback: () => { - this.handleItemMutation(); - }, + this.mutationObserver = new MutationObserver(() => { + this.handleItemMutation(); }); this.host = host; this.host.addController(this); @@ -196,8 +192,16 @@ export class FocusGroupController } clearElementCache(offset = 0): void { + this.mutationObserver.disconnect(); delete this.cachedElements; this.offset = offset; + requestAnimationFrame(() => { + this.elements.forEach((element) => { + this.mutationObserver.observe(element, { + attributes: true, + }); + }); + }); } setCurrentIndexCircularly(diff: number): void { @@ -333,10 +337,23 @@ export class FocusGroupController } hostConnected(): void { + this.recentlyConnected = true; this.addEventListeners(); } hostDisconnected(): void { + this.mutationObserver.disconnect(); this.removeEventListeners(); } + + hostUpdated(): void { + if (this.recentlyConnected) { + this.recentlyConnected = false; + this.elements.forEach((element) => { + this.mutationObserver.observe(element, { + attributes: true, + }); + }); + } + } } diff --git a/tools/reactive-controllers/src/RovingTabindex.ts b/tools/reactive-controllers/src/RovingTabindex.ts index ab2bec0dfe..833b57f274 100644 --- a/tools/reactive-controllers/src/RovingTabindex.ts +++ b/tools/reactive-controllers/src/RovingTabindex.ts @@ -86,7 +86,8 @@ export class RovingTabindexController< super.unmanage(); } - hostUpdated(): void { + override hostUpdated(): void { + super.hostUpdated(); if (!this.host.hasUpdated) { this.manageTabindexes(); }