UNPKG

@furystack/shades-common-components

Version:

Common UI components for FuryStack Shades

603 lines 36 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 { CollectionService } from '../../services/collection-service.js'; import { DataGrid } from './data-grid.js'; describe('DataGrid', () => { beforeEach(() => { document.body.innerHTML = '<div id="root"></div>'; }); afterEach(() => { document.body.innerHTML = ''; }); const createTestService = () => { const service = new CollectionService(); service.data.setValue({ count: 3, entries: [ { id: 1, name: 'First' }, { id: 2, name: 'Second' }, { id: 3, name: 'Third' }, ], }); return service; }; const withTestGrid = async (fn, opts) => { await usingAsync(createInjector(), async (injector) => { await usingAsync(opts?.createService?.() ?? createTestService(), async (service) => { const findOptions = {}; const onFindOptionsChange = vi.fn(); await fn({ injector, service, findOptions, onFindOptionsChange }); }); }); }; describe('rendering', () => { it('should render with columns', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); expect(grid).not.toBeNull(); const headers = grid?.querySelectorAll('th'); expect(headers?.length).toBe(2); }); }); it('should render table structure', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const table = grid?.querySelector('table'); const thead = grid?.querySelector('thead'); const tbody = grid?.querySelector('tbody'); expect(table).not.toBeNull(); expect(thead).not.toBeNull(); expect(tbody).not.toBeNull(); }); }); it('should render custom header components when provided', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: { id: () => createComponent("span", { "data-testid": "custom-header-id" }, "Custom ID Header"), }, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const customHeader = grid?.querySelector('[data-testid="custom-header-id"]'); expect(customHeader).not.toBeNull(); expect(customHeader?.textContent).toBe('Custom ID Header'); }); }); it('should render default header components from headerComponents.default', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: { default: (name) => createComponent("span", { "data-testid": `default-header-${name}` }, "Default: ", name), }, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const defaultHeaderId = grid?.querySelector('[data-testid="default-header-id"]'); const defaultHeaderName = grid?.querySelector('[data-testid="default-header-name"]'); expect(defaultHeaderId?.textContent).toBe('Default: id'); expect(defaultHeaderName?.textContent).toBe('Default: name'); }); }); it('should render DataGridHeader when no custom header is provided', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const defaultHeaders = grid?.querySelectorAll('data-grid-header'); expect(defaultHeaders?.length).toBe(2); }); }); it('should render filter buttons when columnFilters are provided', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, columnFilters: { name: { type: 'string' } } })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const filterButtons = grid?.querySelectorAll('data-grid-filter-button'); expect(filterButtons?.length).toBe(1); }); }); it('should not render filter buttons when columnFilters is not provided', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const filterButtons = grid?.querySelectorAll('data-grid-filter-button'); expect(filterButtons?.length).toBe(0); }); }); it('should render without headerComponents and rowComponents', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); expect(grid).not.toBeNull(); const headers = grid?.querySelectorAll('data-grid-header'); expect(headers?.length).toBe(2); }); }); it('should render with auto-generated data-nav-section attribute', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange })), }); await flushUpdates(); const wrapper = document.querySelector('.shade-grid-wrapper'); const navSection = wrapper?.getAttribute('data-nav-section'); expect(navSection).toBeTruthy(); expect(navSection).toMatch(/^data-grid-/); }); }); it('should render with custom navSection', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, navSection: "my-grid" })), }); await flushUpdates(); const wrapper = document.querySelector('.shade-grid-wrapper'); expect(wrapper?.getAttribute('data-nav-section')).toBe('my-grid'); }); }); }); describe('focus management', () => { it('should clear hasFocus on focusout when focus leaves the grid', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); const outsideBtn = document.createElement('button'); document.body.appendChild(outsideBtn); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange })), }); await flushUpdates(); await new Promise((r) => setTimeout(r, 0)); service.hasFocus.setValue(true); const wrapper = document.querySelector('.shade-grid-wrapper'); wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outsideBtn })); expect(service.hasFocus.getValue()).toBe(false); outsideBtn.remove(); }); }); it('should clear hasFocus on focusout when focus moves outside', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); const outsideEl = document.createElement('button'); outsideEl.textContent = 'Outside'; document.body.appendChild(outsideEl); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange })), }); await flushUpdates(); await new Promise((r) => setTimeout(r, 0)); service.hasFocus.setValue(true); const wrapper = document.querySelector('.shade-grid-wrapper'); wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: outsideEl })); expect(service.hasFocus.getValue()).toBe(false); outsideEl.remove(); }); }); it('should clear hasFocus on focusout when relatedTarget is null', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange })), }); await flushUpdates(); await new Promise((r) => setTimeout(r, 0)); service.hasFocus.setValue(true); const wrapper = document.querySelector('.shade-grid-wrapper'); wrapper?.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: null })); expect(service.hasFocus.getValue()).toBe(false); }); }); }); describe('keyboard navigation', () => { it('should move focus to next entry on ArrowDown', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.hasFocus.setValue(true); service.focusedEntry.setValue(service.data.getValue().entries[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.focusedEntry.getValue()).toEqual({ id: 2, name: 'Second' }); }); }); it('should move focus to previous entry on ArrowUp', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.hasFocus.setValue(true); service.focusedEntry.setValue(service.data.getValue().entries[1]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' }); }); }); it('should handle Home to move focus to first entry', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.hasFocus.setValue(true); service.focusedEntry.setValue(service.data.getValue().entries[2]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: 'Home', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' }); }); }); it('should handle End to move focus to last entry', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.hasFocus.setValue(true); service.focusedEntry.setValue(service.data.getValue().entries[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: 'End', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.focusedEntry.getValue()).toEqual({ id: 3, name: 'Third' }); }); }); it('should handle Escape to clear selection and search', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); const { entries } = service.data.getValue(); service.hasFocus.setValue(true); service.selection.setValue([entries[0], entries[1]]); service.searchTerm.setValue('test'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.selection.getValue()).toEqual([]); expect(service.searchTerm.getValue()).toBe(''); }); }); it('should handle Space to toggle selection of focused entry', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); const { entries } = service.data.getValue(); service.hasFocus.setValue(true); service.focusedEntry.setValue(entries[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: ' ', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.selection.getValue()).toContain(entries[0]); window.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true })); expect(service.selection.getValue()).not.toContain(entries[0]); }); }); it('should handle + to select all entries', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.hasFocus.setValue(true); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: '+', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.selection.getValue().length).toBe(3); }); }); it('should handle - to deselect all entries', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); const { entries } = service.data.getValue(); service.hasFocus.setValue(true); service.selection.setValue([...entries]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: '-', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.selection.getValue().length).toBe(0); }); }); it('should handle * to invert selection', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); const { entries } = service.data.getValue(); service.hasFocus.setValue(true); service.selection.setValue([entries[0]]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: '*', bubbles: true }); window.dispatchEvent(keydownEvent); const selection = service.selection.getValue(); expect(selection).not.toContain(entries[0]); expect(selection).toContain(entries[1]); expect(selection).toContain(entries[2]); }); }); it('should not handle keyboard when not focused', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.focusedEntry.setValue(service.data.getValue().entries[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); service.hasFocus.setValue(false); const keydownEvent = new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' }); }); }); it('should handle Insert to toggle selection and move to next', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); const { entries } = service.data.getValue(); service.hasFocus.setValue(true); service.focusedEntry.setValue(entries[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const keydownEvent = new KeyboardEvent('keydown', { key: 'Insert', bubbles: true }); window.dispatchEvent(keydownEvent); expect(service.selection.getValue()).toContain(entries[0]); expect(service.focusedEntry.getValue()).toEqual(entries[1]); }); }); }); describe('styles', () => { it('should apply wrapper styles when provided', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: { wrapper: { backgroundColor: 'red' }, }, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); expect(grid?.style.backgroundColor).toBe('red'); }); }); it('should apply header styles when provided', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: { header: { color: 'blue' }, }, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const headers = grid?.querySelectorAll('th'); expect(headers?.[0]?.style.color).toBe('blue'); }); }); }); describe('empty and loading states', () => { it('should show empty component when no data', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {}, emptyComponent: createComponent("div", { "data-testid": "empty-state" }, "No data available") })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const emptyState = grid?.querySelector('[data-testid="empty-state"]'); expect(emptyState).not.toBeNull(); expect(emptyState?.textContent).toBe('No data available'); }, { createService: () => new CollectionService() }); }); }); describe('row interactions', () => { it('should pass row click to collectionService', async () => { const onRowClick = vi.fn(); await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { service.addListener('onRowClick', onRowClick); const rootElement = document.getElementById('root'); service.data.setValue({ count: 1, entries: [{ id: 1, name: 'Test' }], }); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const cell = grid?.querySelector('td'); cell?.click(); expect(onRowClick).toHaveBeenCalledWith({ id: 1, name: 'Test' }); }); }); it('should pass row double click to collectionService', async () => { const onRowDoubleClick = vi.fn(); await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { service.addListener('onRowDoubleClick', onRowDoubleClick); const rootElement = document.getElementById('root'); service.data.setValue({ count: 1, entries: [{ id: 1, name: 'Test' }], }); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); const cell = grid?.querySelector('td'); const dblClickEvent = new MouseEvent('dblclick', { bubbles: true }); cell?.dispatchEvent(dblClickEvent); expect(onRowDoubleClick).toHaveBeenCalledWith({ id: 1, name: 'Test' }); }); }); }); describe('row spatial navigation attributes', () => { it('should set data-spatial-nav-target on rows', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange })), }); await flushUpdates(); const rows = document.querySelectorAll('shades-data-grid-row'); for (const row of rows) { expect(row.hasAttribute('data-spatial-nav-target')).toBe(true); } }); }); it('should set tabIndex 0 on focused row and -1 on others', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.focusedEntry.setValue(service.data.getValue().entries[1]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange })), }); await flushUpdates(); const rows = document.querySelectorAll('shades-data-grid-row'); expect(rows[0]?.tabIndex).toBe(-1); expect(rows[1]?.tabIndex).toBe(0); expect(rows[2]?.tabIndex).toBe(-1); }); }); it('should sync focusedEntry on row onfocus', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange })), }); await flushUpdates(); const rows = document.querySelectorAll('shades-data-grid-row'); rows[2]?.dispatchEvent(new FocusEvent('focus')); expect(service.focusedEntry.getValue()).toEqual({ id: 3, name: 'Third' }); expect(service.hasFocus.getValue()).toBe(true); }); }); }); describe('keyboard listener cleanup', () => { it('should remove keyboard listener when component is disconnected', async () => { await withTestGrid(async ({ injector, service, findOptions, onFindOptionsChange }) => { const rootElement = document.getElementById('root'); service.hasFocus.setValue(true); service.focusedEntry.setValue(service.data.getValue().entries[0]); initializeShadeRoot({ injector, rootElement, jsxElement: (createComponent(DataGrid, { columns: ['id', 'name'], collectionService: service, findOptions: findOptions, onFindOptionsChange: onFindOptionsChange, styles: {}, headerComponents: {}, rowComponents: {} })), }); await flushUpdates(); const grid = document.querySelector('shade-data-grid'); grid.remove(); await flushUpdates(); window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true })); expect(service.focusedEntry.getValue()).toEqual({ id: 1, name: 'First' }); }); }); }); }); //# sourceMappingURL=data-grid.spec.js.map