@furystack/shades-common-components
Version:
Common UI components for FuryStack Shades
603 lines • 36 kB
JavaScript
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