UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

735 lines 40.2 kB
import { createInjector } from '@furystack/inject'; import { createComponent, flushUpdates, initializeShadeRoot } from '@furystack/shades'; import { usingAsync } from '@furystack/utils'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { ListService } from '../../services/list-service.js'; import { List } from './list.js'; describe('List', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; }); const testItems = [ { id: 1, name: 'First' }, { id: 2, name: 'Second' }, { id: 3, name: 'Third' }, ]; const createTestService = (options) => { return new ListService(options); }; describe('rendering', () => { it('should render the shade-list custom element', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); expect(list).not.toBeNull(); service[Symbol.dispose](); }); }); it('should render list items', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const items = list?.querySelectorAll('shade-list-item'); expect(items?.length).toBe(3); service[Symbol.dispose](); }); }); it('should render a listbox container', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const listbox = list?.querySelector('[role="listbox"]'); expect(listbox).not.toBeNull(); expect(listbox?.getAttribute('aria-multiselectable')).toBe('true'); service[Symbol.dispose](); }); }); it('should render items with role option', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const options = list?.querySelectorAll('[role="option"]'); expect(options?.length).toBe(3); service[Symbol.dispose](); }); }); it('should render icon when renderIcon is provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), renderIcon: () => createComponent("span", { "data-testid": "icon" }, "icon") })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const icons = list?.querySelectorAll('[data-testid="icon"]'); expect(icons?.length).toBe(3); service[Symbol.dispose](); }); }); it('should render secondary actions when provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), renderSecondaryActions: () => [createComponent("button", { "data-testid": "action" }, "Edit")] })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const actions = list?.querySelectorAll('[data-testid="action"]'); expect(actions?.length).toBe(3); service[Symbol.dispose](); }); }); it('should set data-variant attribute when variant is provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), variant: "contained" })), }); await flushUpdates(); const list = document.querySelector('shade-list'); expect(list?.getAttribute('data-variant')).toBe('contained'); service[Symbol.dispose](); }); }); it('should sync items to the service', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); expect(service.items.getValue()).toEqual(testItems); service[Symbol.dispose](); }); }); }); describe('focus management', () => { it('should set focus on click', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); expect(service.hasFocus.getValue()).toBe(false); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const wrapper = list?.querySelector('.shade-list-wrapper'); wrapper?.click(); expect(service.hasFocus.getValue()).toBe(true); service[Symbol.dispose](); }); }); it('should lose focus on focusout to an outside element', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(createComponent, null, createComponent("button", { "data-testid": "outside" }, "Outside"), createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) }))), }); await flushUpdates(); const list = document.querySelector('shade-list'); const wrapper = list?.querySelector('.shade-list-wrapper'); wrapper?.click(); expect(service.hasFocus.getValue()).toBe(true); const outside = document.querySelector('[data-testid="outside"]'); wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outside })); expect(service.hasFocus.getValue()).toBe(false); service[Symbol.dispose](); }); }); it('should set focused item on item click', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); listItems[1]?.click(); expect(service.focusedItem.getValue()).toBe(testItems[1]); service[Symbol.dispose](); }); }); it('should add focused CSS class to focused item', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); service.focusedItem.setValue(testItems[1]); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); expect(listItems[1]?.hasAttribute('data-focused')).toBe(true); expect(listItems[0]?.hasAttribute('data-focused')).toBe(false); service[Symbol.dispose](); }); }); it('should not initialize focusedItem on wrapper focusin (items handle focus individually)', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); await new Promise((r) => setTimeout(r, 0)); expect(service.hasFocus.getValue()).toBe(false); expect(service.focusedItem.getValue()).toBeUndefined(); service[Symbol.dispose](); }); }); it('should clear hasFocus on focusout when focus moves outside', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); const outsideEl = document.createElement('button'); outsideEl.textContent = 'Outside'; document.body.appendChild(outsideEl); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); await new Promise((r) => setTimeout(r, 0)); service.hasFocus.setValue(true); const wrapper = document.querySelector('.shade-list-wrapper'); wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outsideEl })); expect(service.hasFocus.getValue()).toBe(false); outsideEl.remove(); service[Symbol.dispose](); }); }); it('should clear hasFocus on focusout when relatedTarget is null', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); await new Promise((r) => setTimeout(r, 0)); service.hasFocus.setValue(true); const wrapper = document.querySelector('.shade-list-wrapper'); wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: null })); expect(service.hasFocus.getValue()).toBe(false); service[Symbol.dispose](); }); }); }); describe('selection', () => { it('should add selected CSS class to selected items', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); service.selection.setValue([testItems[0], testItems[2]]); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); expect(listItems[0]?.hasAttribute('data-selected')).toBe(true); expect(listItems[1]?.hasAttribute('data-selected')).toBe(false); expect(listItems[2]?.hasAttribute('data-selected')).toBe(true); service[Symbol.dispose](); }); }); it('should set aria-selected on selected items', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); service.selection.setValue([testItems[0]]); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); expect(listItems[0]?.getAttribute('aria-selected')).toBe('true'); expect(listItems[1]?.getAttribute('aria-selected')).toBe('false'); service[Symbol.dispose](); }); }); it('should call onSelectionChange when selection changes', async () => { const onSelectionChange = vi.fn(); await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), onSelectionChange: onSelectionChange })), }); await flushUpdates(); service.selection.setValue([testItems[0]]); await flushUpdates(); expect(onSelectionChange).toHaveBeenCalledWith([testItems[0]]); service[Symbol.dispose](); }); }); }); describe('keyboard navigation', () => { it('should not handle ArrowDown (delegated to spatial navigation)', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.focusedItem.setValue(testItems[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); expect(service.focusedItem.getValue()).toEqual(testItems[0]); service[Symbol.dispose](); }); }); it('should not handle ArrowUp (delegated to spatial navigation)', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.focusedItem.setValue(testItems[1]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true })); expect(service.focusedItem.getValue()).toEqual(testItems[1]); service[Symbol.dispose](); }); }); it('should handle Home to move focus to first item', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.focusedItem.setValue(testItems[2]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true })); expect(service.focusedItem.getValue()).toEqual(testItems[0]); service[Symbol.dispose](); }); }); it('should handle End to move focus to last item', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.focusedItem.setValue(testItems[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true })); expect(service.focusedItem.getValue()).toEqual(testItems[2]); service[Symbol.dispose](); }); }); it('should handle Space to toggle selection of focused item', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.focusedItem.setValue(testItems[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })); expect(service.selection.getValue()).toContain(testItems[0]); window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })); expect(service.selection.getValue()).not.toContain(testItems[0]); service[Symbol.dispose](); }); }); it('should handle + to select all items', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: '+', bubbles: true })); expect(service.selection.getValue().length).toBe(3); service[Symbol.dispose](); }); }); it('should handle - to deselect all items', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.selection.setValue([...testItems]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: '-', bubbles: true })); expect(service.selection.getValue().length).toBe(0); service[Symbol.dispose](); }); }); it('should handle * to invert selection', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.selection.setValue([testItems[0]]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: '*', bubbles: true })); const selection = service.selection.getValue(); expect(selection).not.toContain(testItems[0]); expect(selection).toContain(testItems[1]); expect(selection).toContain(testItems[2]); service[Symbol.dispose](); }); }); it('should handle Escape to clear selection and search term', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.selection.setValue([testItems[0], testItems[1]]); service.searchTerm.setValue('test'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); expect(service.selection.getValue()).toEqual([]); expect(service.searchTerm.getValue()).toBe(''); service[Symbol.dispose](); }); }); it('should not handle keyboard when not focused', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(false); service.focusedItem.setValue(testItems[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); expect(service.focusedItem.getValue()).toEqual(testItems[0]); service[Symbol.dispose](); }); }); }); describe('activation', () => { it('should call onItemActivate on Enter key', async () => { const onItemActivate = vi.fn(); await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.focusedItem.setValue(testItems[1]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), onItemActivate: onItemActivate })), }); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true })); expect(onItemActivate).toHaveBeenCalledWith(testItems[1]); service[Symbol.dispose](); }); }); it('should call onItemActivate on double-click', async () => { const onItemActivate = vi.fn(); await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), onItemActivate: onItemActivate })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); listItems[0]?.dispatchEvent(new MouseEvent('dblclick', { bubbles: true })); expect(onItemActivate).toHaveBeenCalledWith(testItems[0]); service[Symbol.dispose](); }); }); }); describe('pagination', () => { const manyItems = Array.from({ length: 25 }, (_, i) => ({ id: i + 1, name: `Item ${i + 1}` })); it('should render only current page items when pagination is provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: manyItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), pagination: { itemsPerPage: 10, page: 1, onPageChange: () => { } } })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); expect(listItems?.length).toBe(10); service[Symbol.dispose](); }); }); it('should render the Pagination component', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: manyItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), pagination: { itemsPerPage: 10, page: 1, onPageChange: () => { } } })), }); await flushUpdates(); const pagination = document.querySelector('shade-list shade-pagination'); expect(pagination).not.toBeNull(); service[Symbol.dispose](); }); }); it('should show last page items correctly', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: manyItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), pagination: { itemsPerPage: 10, page: 3, onPageChange: () => { } } })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); expect(listItems?.length).toBe(5); service[Symbol.dispose](); }); }); it('should not render Pagination when all items fit on one page', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), pagination: { itemsPerPage: 10, page: 1, onPageChange: () => { } } })), }); await flushUpdates(); const pagination = document.querySelector('shade-list shade-pagination'); expect(pagination).toBeNull(); service[Symbol.dispose](); }); }); it('should call onPageChange when a pagination button is clicked', async () => { const onPageChange = vi.fn(); await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: manyItems, listService: service, renderItem: (item) => createComponent("span", null, item.name), pagination: { itemsPerPage: 10, page: 1, onPageChange } })), }); await flushUpdates(); const nextButton = document.querySelector('shade-list shade-pagination [aria-label="Go to next page"]'); expect(nextButton).not.toBeNull(); nextButton.click(); expect(onPageChange).toHaveBeenCalledWith(2); service[Symbol.dispose](); }); }); it('should render all items when pagination is not provided', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: manyItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); const listItems = list?.querySelectorAll('shade-list-item'); expect(listItems?.length).toBe(25); const pagination = document.querySelector('shade-list shade-pagination'); expect(pagination).toBeNull(); service[Symbol.dispose](); }); }); }); describe('item spatial navigation attributes', () => { it('should set data-spatial-nav-target on list items', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const items = document.querySelectorAll('shade-list-item'); for (const item of items) { expect(item.hasAttribute('data-spatial-nav-target')).toBe(true); } service[Symbol.dispose](); }); }); it('should set tabIndex 0 on focused item and -1 on others', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.focusedItem.setValue(testItems[1]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const items = document.querySelectorAll('shade-list-item'); expect(items[0]?.tabIndex).toBe(-1); expect(items[1]?.tabIndex).toBe(0); expect(items[2]?.tabIndex).toBe(-1); service[Symbol.dispose](); }); }); it('should sync focusedItem on item onfocus', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const items = document.querySelectorAll('shade-list-item'); items[2]?.dispatchEvent(new FocusEvent('focus')); expect(service.focusedItem.getValue()).toEqual(testItems[2]); expect(service.hasFocus.getValue()).toBe(true); service[Symbol.dispose](); }); }); }); describe('keyboard listener cleanup', () => { it('should remove keyboard listener when component is disconnected', async () => { await usingAsync(createInjector(), async (injector) => { const rootElement = document.getElementById('root'); const service = createTestService(); service.hasFocus.setValue(true); service.focusedItem.setValue(testItems[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(List, { items: testItems, listService: service, renderItem: (item) => createComponent("span", null, item.name) })), }); await flushUpdates(); const list = document.querySelector('shade-list'); list.remove(); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); expect(service.focusedItem.getValue()).toEqual(testItems[0]); service[Symbol.dispose](); }); }); }); }); //# sourceMappingURL=list.spec.js.map