### File

`src/table/data-grid-interaction-model.class.ts`

### Description

`DataGridInteractionModel` provides centralized control over arbitrary 2d grids, following the w3 specs.

Refs:

- https://www.w3.org/TR/wai-aria-practices/examples/grid/dataGrids.html
- https://www.w3.org/TR/wai-aria-practices/#grid

Example usage (taken from `table.component`):

    // a standard HTML table
    const table = this.elementRef.nativeElement.querySelector("table") as HTMLTableElement;

    // `TableDomAdapter` implements `TableAdapter` and provides a consistent interface to query rows and columns in a table
    const tableAdapter = new TableDomAdapter(table);

    // the keydown events that we'll use for keyboard navigation of the table
    const keydownEventStream = fromEvent<KeyboardEvent>(table, "keydown");

    // the click events we'll use to ensure focus is updated correctly on click
    const clickEventStream = fromEvent<MouseEvent>(table, "click");

    // the `DataGridInteractionModel` instance!
    this.interactionModel = new DataGridInteractionModel(keydownEventStream, clickEventStream, tableAdapter);

    // subscribe to the combined position updates
    this.interactionModel.position.subscribe(event => {
        const [currentRow, currentColumn] = event.current;
        const [previousRow, previousColumn] = event.previous;

        // query the TableAdapter for the cell at the current row and column ...
        const currentElement = tableAdapter.getCell(currentRow, currentColumn);
        // ... and make it focusable it
        Table.setTabIndex(currentElement, 0);

        // if the model has just initialized don't focus or reset anything
        if (previousRow === -1 || previousColumn === -1) { return; }

        // query the TableAdapter for the cell at the previous row and column ...
        const previousElement = tableAdapter.getCell(previousRow, previousColumn);
        // ... and make it unfocusable (now there is only a single focusable cell)
        Table.setTabIndex(previousElement, -1);

        // finally, focus the current cell (skipped during initilzation)
        Table.focus(currentElement);
    });

### Index

###### Properties
Readonly columnIndex Protected columnSubject Readonly position Readonly rowIndex Protected rowSubject
###### Methods
goTo goToColumn goToRow handleClickEvent handleKeyboardEvent reset resetTabIndexes
###### Accessors
currentRow currentColumn lastColumn lastRow

### Constructor

`constructor(keyboardEventStream: Observable, clickEventStream: Observable, tableAdapter: TableAdapter)`
Defined in src/table/data-grid-interaction-model.class.ts:116
`DataGridInteractionModel` requires knowledge of events, and a representation of your table/grid to be useful. Parameters :
Name | Type | Optional | Description
keyboardEventStream | `Observable<KeyboardEvent>` | No | `an Observable of KeyboardEvents. Should be scoped to the table container.`
clickEventStream | `Observable<MouseEvent>` | No | `an Observable of ClickEvents. should only include clicks that take action on items known by the TableAdapter`
tableAdapter | `TableAdapter` | No | `an instance of a concrete class that implements TableAdapter. The standard carbon table uses TableDomAdapter`

### Properties

Readonly columnIndex
`columnIndex: `Observable<literal type>``
Type : `Observable<literal type>`
Defined in src/table/data-grid-interaction-model.class.ts:79
An Observable that provides the current and previous column indexes.

Protected columnSubject
`columnSubject: ```
Default value : `new BehaviorSubject({ current: 0, previous: -1 })`
Defined in src/table/data-grid-interaction-model.class.ts:88
Internal subject to handle changes in column

Readonly position
`position: `Observable<DataGridPosition>``
Type : `Observable<DataGridPosition>`
Defined in src/table/data-grid-interaction-model.class.ts:71
An Observable that provides an aggregated view of the `rowIndex` and `columnIndex` Observables

Readonly rowIndex
`rowIndex: `Observable<literal type>``
Type : `Observable<literal type>`
Defined in src/table/data-grid-interaction-model.class.ts:75
An Observable that provides the current and previous row indexes.

Protected rowSubject
`rowSubject: ```
Default value : `new BehaviorSubject({ current: 0, previous: -1 })`
Defined in src/table/data-grid-interaction-model.class.ts:84
Internal subject to handle changes in row

### Methods

goTo
`goTo(undefined)`
Defined in src/table/data-grid-interaction-model.class.ts:238
Jump to the specified row and column Parameters :
Name | Optional
No
Returns : `void`

goToColumn
`goToColumn(index: number)`
Defined in src/table/data-grid-interaction-model.class.ts:218
Jump to a specific column without changing the row Parameters :
Name | Type | Optional | Description
index | `number` | No | `column to jump to`
Returns : `void`

goToRow
`goToRow(index: number)`
Defined in src/table/data-grid-interaction-model.class.ts:228
Jump to a specific row without changing the column Parameters :
Name | Type | Optional | Description
index | `number` | No | `row to jump to`
Returns : `void`

handleClickEvent
`handleClickEvent(event: MouseEvent)`
Defined in src/table/data-grid-interaction-model.class.ts:207
Handles moving the position to the clicked cell Parameters :
Name | Type | Optional | Description
event | `MouseEvent` | No | `the MouseEvent to handle`
Returns : `void`

handleKeyboardEvent
`handleKeyboardEvent(event: KeyboardEvent)`
Defined in src/table/data-grid-interaction-model.class.ts:152
Handles moving the position according to the w3 datagrid navigation specs Refs: https://www.w3.org/TR/wai-aria-practices/examples/grid/dataGrids.html https://www.w3.org/TR/wai-aria-practices/#grid Parameters :
Name | Type | Optional | Description
event | `KeyboardEvent` | No | `the KeyboardEvent to handle`
Returns : `void`

reset
`reset()`
Defined in src/table/data-grid-interaction-model.class.ts:263
Resets the models focus position Returns : `void`

resetTabIndexes
`resetTabIndexes(newTabIndex)`
Defined in src/table/data-grid-interaction-model.class.ts:247
Convenience method to reset the tab indexes on a standard carbon table. For custom tables you may want to reset the indexes manually and simply call `.reset()` Parameters :
Name | Optional | Default value
newTabIndex | No | `-1`
Returns : `void`

### Accessors

currentRow
get `currentRow()`
Defined in src/table/data-grid-interaction-model.class.ts:93
The latest value emitted by the rowSubject

currentColumn
get `currentColumn()`
Defined in src/table/data-grid-interaction-model.class.ts:100
The latest value emitted by the columnSubject

lastColumn
get `lastColumn()`
Defined in src/table/data-grid-interaction-model.class.ts:107
The last column as reported by the adapter

lastRow
get `lastRow()`
Defined in src/table/data-grid-interaction-model.class.ts:114
The last row as reported by the adapter

    import {
    	BehaviorSubject,
    	Observable,
    	combineLatest
    } from "rxjs";
    import { map } from "rxjs/operators";
    import { TableAdapter } from "./table-adapter.class";
    import { tabbableSelectorIgnoreTabIndex, getFocusElementList } from "./../common/tab.service";

    /**
     * The current and previous position in the grid.
     *
     * `current` and `previous` are tuples that follow the `[row, column]` convention.
     */
    export interface DataGridPosition {
    	current: [number, number];
    	previous: [number, number];
    }

    /**
     * `DataGridInteractionModel` provides centralized control over arbitrary 2d grids, following the w3 specs.
     *
     * Refs:
     *  - https://www.w3.org/TR/wai-aria-practices/examples/grid/dataGrids.html
     *  - https://www.w3.org/TR/wai-aria-practices/#grid
     *
     * Example usage (taken from `table.component`):
    ```typescript
    // a standard HTML table
    const table = this.elementRef.nativeElement.querySelector("table") as HTMLTableElement;

    // `TableDomAdapter` implements `TableAdapter` and provides a consistent interface to query rows and columns in a table
    const tableAdapter = new TableDomAdapter(table);

    // the keydown events that we'll use for keyboard navigation of the table
    const keydownEventStream = fromEvent<KeyboardEvent>(table, "keydown");

    // the click events we'll use to ensure focus is updated correctly on click
    const clickEventStream = fromEvent<MouseEvent>(table, "click");

    // the `DataGridInteractionModel` instance!
    this.interactionModel = new DataGridInteractionModel(keydownEventStream, clickEventStream, tableAdapter);

    // subscribe to the combined position updates
    this.interactionModel.position.subscribe(event => {
    	const [currentRow, currentColumn] = event.current;
    	const [previousRow, previousColumn] = event.previous;

    	// query the TableAdapter for the cell at the current row and column ...
    	const currentElement = tableAdapter.getCell(currentRow, currentColumn);
    	// ... and make it focusable it
    	Table.setTabIndex(currentElement, 0);

    	// if the model has just initialized don't focus or reset anything
    	if (previousRow === -1 || previousColumn === -1) { return; }

    	// query the TableAdapter for the cell at the previous row and column ...
    	const previousElement = tableAdapter.getCell(previousRow, previousColumn);
    	// ... and make it unfocusable (now there is only a single focusable cell)
    	Table.setTabIndex(previousElement, -1);

    	// finally, focus the current cell (skipped during initilzation)
    	Table.focus(currentElement);
    });
    ```
     */
    export class DataGridInteractionModel {
    	/**
    	 * An Observable that provides an aggregated view of the `rowIndex` and `columnIndex` Observables
    	 */
    	readonly position: Observable<DataGridPosition>;
    	/**
    	 * An Observable that provides the current and previous row indexes.
    	 */
    	readonly rowIndex: Observable<{ current: number, previous: number }>;
    	/**
    	 * An Observable that provides the current and previous column indexes.
    	 */
    	readonly columnIndex: Observable<{ current: number, previous: number }>;

    	/**
    	 * Internal subject to handle changes in row
    	 */
    	protected rowSubject = new BehaviorSubject({ current: 0, previous: -1 });
    	/**
    	 * Internal subject to handle changes in column
    	 */
    	protected columnSubject = new BehaviorSubject({ current: 0, previous: -1 });

    	/**
    	 * The latest value emitted by the rowSubject
    	 */
    	protected get currentRow() {
    		return this.rowSubject.getValue().current;
    	}

    	/**
    	 * The latest value emitted by the columnSubject
    	 */
    	protected get currentColumn() {
    		return this.columnSubject.getValue().current;
    	}

    	/**
    	 * The last column as reported by the adapter
    	 */
    	protected get lastColumn() {
    		return this.tableAdapter.lastColumnIndex;
    	}

    	/**
    	 * The last row as reported by the adapter
    	 */
    	protected get lastRow() {
    		return this.tableAdapter.lastRowIndex;
    	}

    	/**
    	 * `DataGridInteractionModel` requires knowledge of events, and a representation of your table/grid to be useful.
    	 *
    	 * @param keyboardEventStream an Observable of KeyboardEvents. Should be scoped to the table container.
    	 * @param clickEventStream an Observable of ClickEvents. should only include clicks that take action on items known by the TableAdapter
    	 * @param tableAdapter an instance of a concrete class that implements TableAdapter. The standard carbon table uses TableDomAdapter
    	 */
    	constructor(
    		protected keyboardEventStream: Observable<KeyboardEvent>,
    		protected clickEventStream: Observable<MouseEvent>,
    		protected tableAdapter: TableAdapter
    	) {
    		this.rowIndex = this.rowSubject.asObservable();
    		this.columnIndex = this.columnSubject.asObservable();
    		this.position = combineLatest(this.rowIndex, this.columnIndex).pipe(map(positions => {
    			const [row, column] = positions;
    			return {
    				current: [row.current, column.current],
    				previous: [row.previous, column.previous]
    			};
    		})) as Observable<DataGridPosition>;
    		this.keyboardEventStream.subscribe(this.handleKeyboardEvent.bind(this));
    		this.clickEventStream.subscribe(this.handleClickEvent.bind(this));
    	}

    	/**
    	 * Handles moving the position according to the w3 datagrid navigation specs
    	 *
    	 * Refs:
    	 *  - https://www.w3.org/TR/wai-aria-practices/examples/grid/dataGrids.html
    	 *  - https://www.w3.org/TR/wai-aria-practices/#grid
    	 *
    	 * @param event the KeyboardEvent to handle
    	 */
    	handleKeyboardEvent(event: KeyboardEvent) {
    		const currentCell = this.tableAdapter.getCell(this.currentRow, this.currentColumn);
    		let currentColumn = this.tableAdapter.findColumnIndex(currentCell);
    		let currentRow = this.tableAdapter.findRowIndex(currentCell);

    		switch (event.key) {
    			case "Right": // IE specific value
    			case "ArrowRight":
    				event.preventDefault();
    				// add the colspan since findColumnIndex will return the
    				// first column containing the cell (of N columns it may span)
    				// and we want to navigate to the next "real" column
    				this.goToColumn(currentColumn + currentCell.colSpan);
    				break;
    			case "Left": // IE specific value
    			case "ArrowLeft":
    				event.preventDefault();
    				// we only ever need to subtract 1 from the column, since findColumnIndex returns the
    				// first of N columns containing the cell
    				this.goToColumn(currentColumn - 1);
    				break;
    			case "Down": // IE specific value
    			case "ArrowDown":
    				event.preventDefault();
    				this.goToRow(currentRow + currentCell.rowSpan);
    				break;
    			case "Up": // IE specific value
    			case "ArrowUp":
    				event.preventDefault();
    				this.goToRow(currentRow - 1);
    				break;
    			case "Home":
    				event.preventDefault();
    				if (event.ctrlKey) {
    					this.goTo({row: 0, column: 0});
    				} else {
    					this.goToColumn(0);
    				}
    				break;
    			case "End":
    				event.preventDefault();
    				if (event.ctrlKey) {
    					this.goTo({ row: this.lastRow, column: this.lastColumn });
    				} else {
    					this.goToColumn(this.lastColumn);
    				}
    				break;
    		}
    	}

    	/**
    	 * Handles moving the position to the clicked cell
    	 *
    	 * @param event the MouseEvent to handle
    	 */
    	handleClickEvent(event: MouseEvent) {
    		const cell = (event.target as HTMLElement).closest("td, th") as HTMLTableCellElement;
    		const [rowIndex, cellIndex] = this.tableAdapter.findIndex(cell);
    		this.goTo({ row: rowIndex, column: cellIndex });
    	}

    	/**
    	 * Jump to a specific column without changing the row
    	 *
    	 * @param index column to jump to
    	 */
    	goToColumn(index: number) {
    		if (index > this.lastColumn || index < 0) { return; }
    		this.goTo({ row: this.currentRow, column: index});
    	}

    	/**
    	 * Jump to a specific row without changing the column
    	 *
    	 * @param index row to jump to
    	 */
    	goToRow(index: number) {
    		if (index > this.lastRow || index < 0) { return; }
    		this.goTo({row: index, column: this.currentColumn});
    	}

    	/**
    	 * Jump to the specified row and column
    	 *
    	 * @param param0 an object that contains `row` and `column` properties
    	 */
    	goTo({row, column}) {
    		this.rowSubject.next({ current: row, previous: this.currentRow });
    		this.columnSubject.next({ current: column, previous: this.currentColumn });
    	}

    	/**
    	 * Convenience method to reset the tab indexes on a standard carbon table.
    	 * For custom tables you may want to reset the indexes manually and simply call `.reset()`
    	 */
    	resetTabIndexes(newTabIndex = -1) {
    		for (let i = 0; i < this.tableAdapter.lastRowIndex; i++) {
    			const row = this.tableAdapter.getRow(i) as HTMLTableRowElement;
    			for (const cell of Array.from(row.cells)) {
    				const tabbableElements = getFocusElementList(cell, tabbableSelectorIgnoreTabIndex);
    				tabbableElements.forEach((node: HTMLElement) => node.tabIndex = newTabIndex);
    				cell.tabIndex = newTabIndex;
    			}
    		}

    		this.reset();
    	}

    	/**
    	 * Resets the models focus position
    	 */
    	reset() {
    		this.rowSubject.next({ current: 0, previous: -1 });
    		this.columnSubject.next({ current: 0, previous: -1 });
    	}
    }

# result-matching ""

# No results matching ""