Skip to content
This repository has been archived by the owner on May 24, 2024. It is now read-only.

Commit

Permalink
Data grid add column actions (#2024)
Browse files Browse the repository at this point in the history
  • Loading branch information
adoroshk authored Feb 28, 2024
1 parent b02156e commit 365c26a
Show file tree
Hide file tree
Showing 500 changed files with 4,090 additions and 964 deletions.
13 changes: 7 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ const CompactInteractiveList = (props) => {
}, [listRef, focusedCell, columns]);

const focusCell = ({ row, cell }) => {
// add 1 to the row number to accomodate for hidden header
// add 1 to the row number to accommodate for hidden header
const focusedCellElement = listRef.current.children[row + 1].children[cell];
const interactiveChildren = getFocusableElements(focusedCellElement);
if (interactiveChildren?.length > 0 && interactiveChildren[0]) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,7 @@ export const getFocusedCellIndexes = (list, columns, ids) => {
const { rowId, columnId } = ids;
const row = [...list.children].findIndex(rowElement => rowElement.getAttribute('data-row-id') === rowId);
const cell = columns.findIndex(col => col.id === columnId);
// row - 1 needs to accomodate for the hidden header row
// row - 1 needs to accommodate for the hidden header row
return { row: row - 1, cell };
};

Expand All @@ -321,7 +321,7 @@ export const getFocusedCellIndexes = (list, columns, ids) => {
*/
export const getFocusedCellIds = (list, columns, indexes) => {
const { row, cell } = indexes;
// row + 1 needs to accomodate for the hidden header row
// row + 1 needs to accommodate for the hidden header row
const rowId = list.children[row + 1].getAttribute('data-row-id');
const columnId = columns[cell].id;
return { rowId, columnId };
Expand Down
2 changes: 1 addition & 1 deletion packages/terra-compact-interactive-list/src/utils/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export const checkIfRowHasResponsiveColumns = (columns) => columns.reduce(checkI
*/
export const converseColumnTypes = (columns, defaultType) => {
let unitType;
// get unitType and check it's consistant across columns
// get unitType and check it's consistent across columns

let i = 0;
while (!unitType && i < columns?.length) {
Expand Down
5 changes: 4 additions & 1 deletion packages/terra-data-grid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

## Unreleased

* Added
* Added support for column actions.

## 1.15.0 - (February 28, 2024)

* Added `boundingRef` prop for bounded flowsheet data grids.
Expand All @@ -14,7 +17,7 @@
## 1.13.0 - (February 16, 2024)

* Changed
* Removed the default rowMinimumHeight from FlowsheetDataGrid so that the `terra-table` default value is used.
* Removed the default rowMinimumHeight from FlowsheetDataGrid so that the `terra-table` default value is used.

## 1.12.0 - (February 1, 2024)

Expand Down
36 changes: 26 additions & 10 deletions packages/terra-data-grid/src/DataGrid.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
import classNames from 'classnames/bind';
import * as KeyCode from 'keycode-js';
import Table, {
GridConstants, GridContext, sectionShape, rowShape, columnShape, validateRowHeaderIndex,
GridConstants, GridContext, sectionShape, rowShape, columnShape, validateRowHeaderIndex, hasColumnActions,
} from 'terra-table';
import VisuallyHiddenText from 'terra-visually-hidden-text';
import WorklistDataGridUtils from './utils/WorklistDataGridUtils';
Expand Down Expand Up @@ -210,8 +210,15 @@ const DataGrid = forwardRef((props, ref) => {

const [checkResizable, setCheckResizable] = useState(false);

// if columns are not visible then set the first selectable row index to 1
const [focusedRow, setFocusedRow] = useState(hasVisibleColumnHeaders ? 0 : 1);
// check if at least one column has an action prop
// same check is done in Table, but as Table can be a stand-alone component, it can't rely on a passed prop.
const hasColumnHeaderActions = hasColumnActions(pinnedColumns) || hasColumnActions(overflowColumns);

// eslint-disable-next-line no-nested-ternary
const firstRowIndex = hasVisibleColumnHeaders ? 0 : 1;

// if columns are not visible then set the first selectable row index to 1 or 2
const [focusedRow, setFocusedRow] = useState(firstRowIndex);
const [focusedCol, setFocusedCol] = useState(0);

// Aria live region message management
Expand Down Expand Up @@ -261,14 +268,15 @@ const DataGrid = forwardRef((props, ref) => {
}

// Set focus to column header button, if it exists
if (newRowIndex === 0 && !focusedCell.hasAttribute('tabindex')) {
focusedCell = focusedCell.querySelector('[role="button"]');
const isHeaderRow = (newRowIndex === 0 || (hasColumnHeaderActions && newRowIndex === 1));
if (isHeaderRow && !focusedCell.hasAttribute('tabindex')) {
focusedCell = focusedCell.querySelector('[role="button"]') || focusedCell.querySelector('button');
}
}

focusedCell?.focus();
}
}, [displayedColumns, isSection, isRowSelectionCell]);
}, [displayedColumns, isSection, isRowSelectionCell, hasColumnHeaderActions]);

// The focus is handled by the DataGrid. However, there are times
// when the other components may want to change the currently focus
Expand Down Expand Up @@ -326,6 +334,11 @@ const DataGrid = forwardRef((props, ref) => {
setFocusedRowCol(toCell.row, toCell.col, true);
};

// callBack to trigger re-focusing when focused row or col didn't change, but focus update is needed
const triggerFocus = useCallback(() => (
setFocusedRowCol(focusedRow, focusedCol, true)
), [setFocusedRowCol, focusedRow, focusedCol]);

// -------------------------------------
// event handlers

Expand Down Expand Up @@ -431,7 +444,7 @@ const DataGrid = forwardRef((props, ref) => {
} else {
// Left key
nextCol -= 1;
setCheckResizable(cellCoordinates.row === 0);
setCheckResizable(cellCoordinates.row === 0 || (hasColumnHeaderActions && cellCoordinates.row === 1));
}
break;
case KeyCode.KEY_RIGHT:
Expand Down Expand Up @@ -491,7 +504,7 @@ const DataGrid = forwardRef((props, ref) => {
event.preventDefault(); // prevent the page from moving with the arrow keys.
return;
}
if (nextCol < 0 || nextRow < (hasVisibleColumnHeaders ? 0 : 1)) {
if (nextCol < 0 || nextRow < (firstRowIndex)) {
event.preventDefault(); // prevent the page from moving with the arrow keys.
return;
}
Expand Down Expand Up @@ -553,6 +566,7 @@ const DataGrid = forwardRef((props, ref) => {
// -------------------------------------

const isGridActive = grid.current?.contains(document.activeElement);
const isOneOfHeaderRows = focusedRow === 0 || (hasColumnHeaderActions && focusedRow === 1);

return (
// eslint-disable-next-line jsx-a11y/no-static-element-interactions
Expand All @@ -573,8 +587,10 @@ const DataGrid = forwardRef((props, ref) => {
sections={sections}
ariaLabelledBy={ariaLabelledBy}
ariaLabel={ariaLabel}
activeColumnIndex={(isGridActive && focusedRow === 0) ? focusedCol : undefined}
isActiveColumnResizing={focusedRow === 0 && checkResizable}
activeColumnIndex={(isGridActive && (focusedRow === 0 || (hasColumnHeaderActions && focusedRow === 1))) ? focusedCol : undefined}
focusedRowIndex={focusedRow}
triggerFocus={triggerFocus}
isActiveColumnResizing={isOneOfHeaderRows && checkResizable}
columnResizeIncrement={columnResizeIncrement}
pinnedColumns={pinnedColumns}
overflowColumns={overflowColumns}
Expand Down
135 changes: 135 additions & 0 deletions packages/terra-data-grid/tests/jest/WorklistDataGrid.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import { v4 as uuidv4 } from 'uuid';
import WorklistDataGrid from '../../src/WorklistDataGrid';

const mockAction = jest.fn();
// Source data for tests
const dataFile = {
cols: [
Expand All @@ -15,6 +16,24 @@ const dataFile = {
id: 'Column-2', displayName: 'March 17', isSelectable: false, isResizable: true,
},
],
colsWithActions: [
{
id: 'Column-0',
displayName: ' Vitals',
isSelectable: true,
isResizable: true,
action: {
label: 'action button',
onClick: mockAction,
},
},
{
id: 'Column-1', displayName: 'March 16', isSelectable: true, isResizable: true,
},
{
id: 'Column-2', displayName: 'March 17', isSelectable: false, isResizable: true,
},
],
rows: [
{
id: '1',
Expand Down Expand Up @@ -605,3 +624,119 @@ describe('Row selection', () => {
expect(wrapper).toMatchSnapshot();
});
});

describe('Column Header with Actions keyboard navigation', () => {
const arrowRightProps = {
key: 'ArrowRight', keyCode: 39, which: 39,
};
const arrowLeftProps = {
key: 'ArrowLeft', keyCode: 37, which: 37,
};
const arrowDownProps = {
key: 'ArrowDown', keyCode: 40, which: 40,
};
const arrowUpProps = {
key: 'ArrowUp', keyCode: 38, which: 38,
};

beforeEach(() => {
document.getElementsByTagName('html')[0].innerHTML = '';
});

it('Validate LEFT key on resize handle navigates back to cell it came from', () => {
const wrapper = enzymeIntl.mountWithIntl(
<WorklistDataGrid
id="test-terra-worklist-data-grid"
pinnedColumns={dataFile.colsWithActions.slice(0, 2)}
overflowColumns={dataFile.colsWithActions.slice(2)}
rows={dataFile.rows}
/>, {
attachTo: document.body,
},
);

const headerCell = wrapper.find('.header-container').at(0);
const actionCell = wrapper.find('.action-cell').at(0);
const actionButton = actionCell.find('button');
const resizeHandle = wrapper.find('.resize-handle').at(0);

// step DOWN from header cell should focus on action button
headerCell.simulate('keydown', arrowDownProps);
expect(document.activeElement).toBe(actionButton.instance());

// step RIGHT from action button should focus on resize handle
actionButton.simulate('keydown', arrowRightProps);
expect(document.activeElement).toBe(resizeHandle.instance());

// step LEFT from resize handle should focus back on action button
resizeHandle.simulate('keydown', arrowLeftProps);
expect(document.activeElement).toBe(actionButton.instance());

// step UP from action button to header cell
actionButton.simulate('keydown', arrowUpProps);
expect(document.activeElement).toBe(headerCell.instance());

// step RIGHT from header cell should focus on resize handle
headerCell.simulate('keydown', arrowRightProps);
expect(document.activeElement).toBe(resizeHandle.instance());

// step LEFT from resize handle should focus back on header cell
resizeHandle.simulate('keydown', arrowLeftProps);
expect(document.activeElement).toBe(headerCell.instance());
});

it('Validate RIGHT key on resize handle navigates to the next column keeping the row', () => {
const wrapper = enzymeIntl.mountWithIntl(
<WorklistDataGrid
id="test-terra-worklist-data-grid"
pinnedColumns={dataFile.colsWithActions.slice(0, 2)}
overflowColumns={dataFile.colsWithActions.slice(2)}
rows={dataFile.rows}
/>, {
attachTo: document.body,
},
);

const headerCell = wrapper.find('.header-container').at(0);
const headerCell2 = wrapper.find('.header-container').at(1);
const actionButton = wrapper.find('.action-cell').at(0).find('button');
const actionButton2 = wrapper.find('.action-cell').at(1); // There is no button inside placeholder cell
const resizeHandle = wrapper.find('.resize-handle').at(0);

// step DOWN from header cell should focus on action button
headerCell.simulate('keydown', arrowDownProps);
expect(document.activeElement).toBe(actionButton.instance());

// step RIGHT from action button should focus on resize handle
actionButton.simulate('keydown', arrowRightProps);
expect(document.activeElement).toBe(resizeHandle.instance());

// step RIGHT from resize handle should focus on action placeholder cell in the second column
resizeHandle.simulate('keydown', arrowRightProps);
expect(document.activeElement).toBe(actionButton2.instance());

// step LEFT from second col action placeholder should focus on resize handle
actionButton2.simulate('keydown', arrowLeftProps);
expect(document.activeElement).toBe(resizeHandle.instance());

// step LEFT from resize handle should focus on first action button
resizeHandle.simulate('keydown', arrowLeftProps);
expect(document.activeElement).toBe(actionButton.instance());

// step UP from action button to header cell
actionButton.simulate('keydown', arrowUpProps);
expect(document.activeElement).toBe(headerCell.instance());

// step RIGHT from header cell should focus on resize handle
headerCell.simulate('keydown', arrowRightProps);
expect(document.activeElement).toBe(resizeHandle.instance());

// step RIGHT from resize handle should focus on second col header cell
resizeHandle.simulate('keydown', arrowRightProps);
expect(document.activeElement).toBe(headerCell2.instance());

// step LEFT from headerCell in second col should focus back on resize handle
headerCell2.simulate('keydown', arrowLeftProps);
expect(document.activeElement).toBe(resizeHandle.instance());
});
});
Loading

0 comments on commit 365c26a

Please sign in to comment.