import { ROW_FOCUS_ID } from '../constants' import type { Emitter } from '../events' import { createEmitter } from '../events' import type { GridState, Store } from '../state' import { createStore, INITIAL_STATE } from '../state' import type { GridFocusApi } from './focus' import { createFocusApi } from './focus' const getStateMock = (state: Partial = {}): GridState => ({ ...INITIAL_STATE, columns: { ...INITIAL_STATE.columns, ids: ['First', 'Second'], }, rows: { ...INITIAL_STATE.rows, }, ...state, }) describe('focus api', () => { let store: Store, api: GridFocusApi, events: Emitter beforeEach(() => { store = createStore() jest.spyOn(store, 'getState').mockReturnValue(getStateMock()) jest.spyOn(store.selectors, 'selectHeaderRowCount') jest.spyOn(store.selectors, 'selectHeaderHierarchy') jest.spyOn(store.selectors, 'selectCurrentFocus') jest.spyOn(store.selectors, 'selectRowIds').mockReturnValue([ '101', '102', '103', ]) jest.spyOn(store.selectors, 'selectHeaderRowIds').mockReturnValue(['0']) jest.spyOn(store.selectors, 'selectColumnSpanByRowId').mockReturnValue( null ) store.dispatch = jest.fn() events = createEmitter() api = createFocusApi(store, events) }) describe('move', () => { describe('when in header', () => { it('should not move left if on first header cell', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'header', columnId: '', rowId: '0', subFocus: 'first', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: '', rowId: '0', subFocus: 'first', }, }) }) it('should move left if on other header cells', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'header', columnId: 'Second', rowId: '0', subFocus: 'first', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'First', rowId: '0', subFocus: 'last', }, }) }) it('should not move right if on last header cell', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'header', columnId: 'Second', rowId: '', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'Second', rowId: '', subFocus: 'first', }, }) }) it('should move right if on other header cells', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'header', columnId: 'First', rowId: '', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'Second', rowId: '', subFocus: 'first', }, }) }) it('should move to first row in body if on last row of header', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'header', columnId: 'First', rowId: '', subFocus: 'first', } ) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '101', subFocus: 'first', }, }) }) it('should not move up if on first row of header', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'header', columnId: 'First', rowId: '0', subFocus: 'first', } ) api.move('up') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'First', rowId: '0', subFocus: 'first', }, }) }) it('should move up if not on first row of header', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'header', columnId: 'First', rowId: '0', subFocus: 'first', } ) api.move('up') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'First', rowId: '0', subFocus: 'first', }, }) }) describe('with grouped headers', () => { beforeEach(() => { jest.spyOn(store, 'getState').mockReturnValue( getStateMock({ columns: { ...INITIAL_STATE.columns, ids: [ 'column0', 'column1', 'column2', 'column3', 'column4', ], }, }) ) jest.mocked( store.selectors.selectHeaderHierarchy ).mockReturnValue({ levels: 2, groups: [ { id: 'top', level: 2, childIds: ['group1', 'group2'], columnIds: [ 'column1', 'column2', 'column3', 'column4', ], parentIds: [], width: 4, }, { id: 'group1', level: 1, childIds: ['column1', 'column2'], columnIds: ['column1', 'column2'], parentIds: [], width: 2, }, { id: 'group2', level: 1, childIds: ['column3', 'column4'], columnIds: ['column3', 'column4'], parentIds: [], width: 2, }, ], }) jest.mocked( store.selectors.selectHeaderRowIds ).mockReturnValue(['0', '1', '2']) }) it('should move to top group from column', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column0', rowId: '0', subFocus: 'first', }) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column1', rowId: '0', subFocus: 'first', }, }) }) it('should not move when moving at right most group', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column1', rowId: '0', subFocus: 'first', }) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column1', rowId: '0', subFocus: 'first', }, }) }) it('should not move when moving at top most group', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column1', rowId: '0', subFocus: 'first', }) api.move('up') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column1', rowId: '0', subFocus: 'first', }, }) }) it('should move all the way back when at last column in top group', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column4', rowId: '0', subFocus: 'first', }) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column0', rowId: '0', subFocus: 'last', }, }) }) it('should move down to group', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column4', rowId: '0', subFocus: 'first', }) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column4', rowId: '1', subFocus: 'first', }, }) }) it('should move down to column', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column4', rowId: '1', subFocus: 'first', }) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column4', rowId: '2', subFocus: 'first', }, }) }) it('should move down to body', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column4', rowId: '2', subFocus: 'first', }) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column4', rowId: '101', subFocus: 'first', }, }) }) it('should move down to body if no parents in column', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column0', rowId: '0', subFocus: 'first', }) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column0', rowId: '101', subFocus: 'first', }, }) }) it('should move right to next group', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column1', rowId: '1', subFocus: 'first', }) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column3', rowId: '1', subFocus: 'first', }, }) }) it('should move left to previous group', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'header', columnId: 'column4', rowId: '1', subFocus: 'first', }) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'column2', rowId: '1', subFocus: 'last', }, }) }) }) }) describe('when in body', () => { it('should not move left if on first body cell', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'First', rowId: '101', subFocus: 'first', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '101', subFocus: 'first', }, }) }) it('should move left and loop around if on the row and loopHorizontally is on', () => { jest.spyOn( store.selectors, 'selectCanLoopHorizontally' ).mockReturnValue(true) jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: ROW_FOCUS_ID, rowId: '101', subFocus: 'first', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'Second', rowId: '101', subFocus: 'last', }, }) }) it('should move left if on other body cells', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'Second', rowId: '0', subFocus: 'first', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '0', subFocus: 'last', }, }) }) describe('with spanned cells', () => { beforeEach(() => { jest.spyOn(store, 'getState').mockReturnValue( getStateMock({ columns: { ...INITIAL_STATE.columns, ids: [ 'column0', 'column1', 'column2', 'column3', 'column4', ], }, }) ) jest.mocked( store.selectors.selectColumnSpanByRowId ).mockReturnValue( new Map([ [ 'column0', { id: 'column0', positionColumnIds: ['column0', 'column1'], }, ], ['column1', { id: 'column1', skip: true }], [ 'column3', { id: 'column3', positionColumnIds: ['column3', 'column4'], }, ], ['column4', { id: 'column4', skip: true }], ]) ) }) it('should move left past spanned cell if focus is within a spanned cell (source cell)', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column3', rowId: '101', subFocus: 'first', }) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column2', rowId: '101', subFocus: 'last', }, }) }) it('should move left past spanned cell to row selection if focus is within a spanned cell (source cell)', () => { jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column1', rowId: '101', subFocus: 'first', }) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: ROW_FOCUS_ID, rowId: '101', subFocus: 'last', }, }) }) it('should move left past spanned cell if focus is within a spanned cell (skipped cell)', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column4', rowId: '101', subFocus: 'first', }) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column2', rowId: '101', subFocus: 'last', }, }) }) it('should not move left if on spanned cell at start of row', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column1', rowId: '101', subFocus: 'first', }) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column1', rowId: '101', subFocus: 'first', }, }) }) }) it('should not move right if on last body cell', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'Second', rowId: '0', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'Second', rowId: '0', subFocus: 'first', }, }) }) it('should move right and loop around if on the row and loopHorizontally is on', () => { jest.spyOn( store.selectors, 'selectCanLoopHorizontally' ).mockReturnValue(true) jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: ROW_FOCUS_ID, rowId: '101', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '101', subFocus: 'first', }, }) }) it('should move right if on other body cells', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: '0', rowId: '101', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '101', subFocus: 'first', }, }) }) describe('with spanned cells', () => { beforeEach(() => { jest.spyOn(store, 'getState').mockReturnValue( getStateMock({ columns: { ...INITIAL_STATE.columns, ids: [ 'column0', 'column1', 'column2', 'column3', 'column4', ], }, }) ) jest.mocked( store.selectors.selectColumnSpanByRowId ).mockReturnValue( new Map([ [ 'column0', { id: 'column0', positionColumnIds: ['column0', 'column1'], }, ], ['column1', { id: 'column1', skip: true }], [ 'column3', { id: 'column3', positionColumnIds: ['column3', 'column4'], }, ], ['column4', { id: 'column4', skip: true }], ]) ) }) it('should move right past spanned cell if focus is within a spanned cell (source cell)', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column0', rowId: '101', subFocus: 'first', }) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column2', rowId: '101', subFocus: 'first', }, }) }) it('should move right past spanned cell to row selection if focus is within a spanned cell (source cell)', () => { jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column3', rowId: '101', subFocus: 'first', }) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: ROW_FOCUS_ID, rowId: '101', subFocus: 'first', }, }) }) it('should move right past spanned cell if focus is within a spanned cell (skipped cell)', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column1', rowId: '101', subFocus: 'first', }) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column2', rowId: '101', subFocus: 'first', }, }) }) it('should not move right if on spanned cell at end of row', () => { jest.mocked( store.selectors.selectCurrentFocus ).mockReturnValue({ area: 'body', columnId: 'column3', rowId: '101', subFocus: 'first', }) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'column3', rowId: '101', subFocus: 'first', }, }) }) }) it('should move to last row in header if on first row of body', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'First', rowId: '101', subFocus: 'first', } ) api.move('up') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: 'First', rowId: '0', subFocus: 'first', }, }) }) it('should move up if not on first row of body', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'First', rowId: '102', subFocus: 'first', } ) api.move('up') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '101', subFocus: 'first', }, }) }) it('should move down if not on last row of body', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'First', rowId: '102', subFocus: 'first', } ) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '103', subFocus: 'first', }, }) }) it('should not move down if on last row of body', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'First', rowId: '103', subFocus: 'first', } ) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '103', subFocus: 'first', }, }) }) it('should move to footer if aggregation is enabled', () => { jest.spyOn( store.selectors, 'selectAggregationEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'First', rowId: '103', subFocus: 'first', } ) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'footer', columnId: 'First', rowId: '0', subFocus: 'first', }, }) }) it('should focus row if drag-and-drop is enabled and move to left', () => { jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'First', rowId: '103', subFocus: 'first', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: ROW_FOCUS_ID, rowId: '103', subFocus: 'last', }, }) }) it('should not move when row focused at start and move to left', () => { jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: ROW_FOCUS_ID, rowId: '103', subFocus: 'last', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: ROW_FOCUS_ID, rowId: '103', subFocus: 'last', }, }) }) it('should focus row if drag-and-drop is enabled and move to right', () => { jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: 'Second', rowId: '103', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: ROW_FOCUS_ID, rowId: '103', subFocus: 'first', }, }) }) it('should not move if row is focused at end and move to right', () => { jest.spyOn( store.selectors, 'selectIsDraggingEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: ROW_FOCUS_ID, rowId: '103', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: ROW_FOCUS_ID, rowId: '103', subFocus: 'first', }, }) }) it('should move to last row in header with row focused if on first row of body', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: ROW_FOCUS_ID, rowId: '101', subFocus: 'first', } ) api.move('up') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'header', columnId: ROW_FOCUS_ID, rowId: '0', subFocus: 'first', }, }) }) it('should move to footer with row focused if aggregation is enabled', () => { jest.spyOn( store.selectors, 'selectAggregationEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'body', columnId: ROW_FOCUS_ID, rowId: '103', subFocus: 'first', } ) api.move('down') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'footer', columnId: ROW_FOCUS_ID, rowId: '0', subFocus: 'first', }, }) }) }) describe('when in footer', () => { it('should move to body if in footer', () => { jest.spyOn( store.selectors, 'selectAggregationEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'footer', columnId: 'First', rowId: '0', subFocus: 'first', } ) api.move('up') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'First', rowId: '103', subFocus: 'first', }, }) }) it('should move right in footer', () => { jest.spyOn( store.selectors, 'selectAggregationEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'footer', columnId: 'First', rowId: '0', subFocus: 'first', } ) api.move('right') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'footer', columnId: 'Second', rowId: '0', subFocus: 'first', }, }) }) it('should move left in footer', () => { jest.spyOn( store.selectors, 'selectAggregationEnabled' ).mockReturnValue(true) jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue( { area: 'footer', columnId: 'Second', rowId: '0', subFocus: 'first', } ) api.move('left') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'footer', columnId: 'First', rowId: '0', subFocus: 'last', }, }) }) }) }) describe('set', () => { it('should dispatch with current subFocus if not set', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue({ area: 'header', columnId: '', rowId: '0', subFocus: 'first', }) api.set('123', 'name', 'body') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'name', rowId: '123', subFocus: 'first', }, }) }) it('should dispatch with subFocus if set', () => { jest.mocked(store.selectors.selectCurrentFocus).mockReturnValue({ area: 'header', columnId: '', rowId: '0', subFocus: 'first', }) api.set('123', 'name', 'body', 'last') expect(store.dispatch).toHaveBeenCalledWith({ type: 'updateFocus', payload: { area: 'body', columnId: 'name', rowId: '123', subFocus: 'last', }, }) }) }) })