Skip to content

Commit 2f82eef

Browse files
feat: cell-level keyboard navigation for Table (#1211)
BREAKING CHANGE * feat: table with inputs, initial edit mode code * feat: keyboardNavigation prop * fix: passing compact prop, focus management, dev story * fix: focusable styling * chore: tests * fix: snapshots * fix: add screen reader text * fix: storyshots * fix: improve screen reader instructions * fix: storyshot * fix: role * fix: storyshots * feat: version upgrade to fd-styles 0.12.0-rc.64 * Revert "feat: version upgrade to fd-styles 0.12.0-rc.64" This reverts commit 262b97d. * fix: syles upgrade to incorrect branch revert to 0.12.0-rc.44 * fix: pr feedback * fix: storyshot * fix: size limit 240 KB * fix: typos * fix: Table snapshot Co-authored-by: Mike <[email protected]>
1 parent 6c26d7d commit 2f82eef

File tree

9 files changed

+1684
-69
lines changed

9 files changed

+1684
-69
lines changed

.size-limit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,6 @@
33
name: "Fundamental-React Size",
44
webpack: true,
55
path: "lib/index.js",
6-
limit: "236 KB"
6+
limit: "240 KB"
77
}
88
]

src/Popover/Popover.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import Popper from '../utils/_Popper';
88
import PropTypes from 'prop-types';
99
import shortId from '../utils/shortId';
1010
import tabbable from 'tabbable';
11-
import { POPOVER_TYPES, POPPER_PLACEMENTS, POPPER_SIZING_TYPES } from '../utils/constants';
11+
import { GridSelector, POPOVER_TYPES, POPPER_PLACEMENTS, POPPER_SIZING_TYPES } from '../utils/constants';
1212
import React, { Component } from 'react';
1313
import 'fundamental-styles/dist/popover.css';
1414

@@ -71,10 +71,10 @@ class Popover extends Component {
7171
this.handleOutsideClick();
7272

7373
if (this.controlRef) {
74-
if (tabbable.isTabbable(this.controlRef)) {
74+
if (tabbable.isFocusable(this.controlRef)) {
7575
this.controlRef.focus();
7676
} else {
77-
const firstTabbableNode = tabbable(this.controlRef)[0];
77+
const firstTabbableNode = this.controlRef.querySelectorAll(GridSelector.FOCUSABLE)[0];
7878
firstTabbableNode && firstTabbableNode.focus();
7979
}
8080
}

src/Table/Table.js

Lines changed: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,23 @@
11
import classnames from 'classnames';
2+
import CustomPropTypes from '../utils/CustomPropTypes/CustomPropTypes';
3+
import GridManager from '../utils/gridManager/gridManager';
4+
import keycode from 'keycode';
25
import PropTypes from 'prop-types';
3-
import React from 'react';
6+
import shortid from 'shortid';
7+
import React, { useCallback, useRef, useState } from 'react';
48
import 'fundamental-styles/dist/table.css';
59

610
/** A **Table** is a set of tabular data. Line items can support `data`, `images` and `actions`. */
7-
const Table = React.forwardRef(({ headers, tableData, className, tableBodyClassName,
11+
const Table = React.forwardRef(({ headers, tableData, className, compact, condensed, keyboardNavigation, localizedText, tableBodyClassName,
812
tableBodyProps, tableBodyRowProps, tableCellClassName, tableCheckboxClassName, tableHeaderClassName, tableHeaderProps,
913
tableHeaderRowClassName, tableHeaderRowProps, tableRowClassName, richTable, ...props }, ref) => {
1014

1115
const tableClasses = classnames(
1216
'fd-table',
17+
{
18+
'fd-table--compact': compact,
19+
'fd-table--condensed': condensed
20+
},
1321
className
1422
);
1523

@@ -20,6 +28,9 @@ const Table = React.forwardRef(({ headers, tableData, className, tableBodyClassN
2028

2129
const tableHeaderRowClasses = classnames(
2230
'fd-table__row',
31+
{
32+
'fd-table__row--focusable': keyboardNavigation === 'row'
33+
},
2334
tableHeaderRowClassName
2435
);
2536

@@ -30,11 +41,17 @@ const Table = React.forwardRef(({ headers, tableData, className, tableBodyClassN
3041

3142
const tableRowClasses = classnames(
3243
'fd-table__row',
44+
{
45+
'fd-table__cell--focusable': keyboardNavigation === 'row'
46+
},
3347
tableRowClassName
3448
);
3549

3650
const tableCellClasses = classnames(
3751
'fd-table__cell',
52+
{
53+
'fd-table__cell--focusable': keyboardNavigation === 'cell'
54+
},
3855
tableCellClassName
3956
);
4057

@@ -44,17 +61,67 @@ const Table = React.forwardRef(({ headers, tableData, className, tableBodyClassN
4461
tableCheckboxClassName
4562
);
4663

64+
const useHookWithRefCallback = () => {
65+
const newRef = useRef(null);
66+
const setRef = useCallback(node => {
67+
if (node && keyboardNavigation !== 'none') {
68+
gridManager.current.attachTo({ gridNode: node, onFocusCell, onToggleEditMode });
69+
}
70+
newRef.current = node;
71+
}, []);
72+
73+
return setRef;
74+
};
75+
76+
const captionId = shortid.generate();
77+
const gridManager = useRef(new GridManager());
78+
const tableRef = ref || useHookWithRefCallback();
79+
const [instructionsText, setInstructionsText] = useState('');
80+
81+
const onToggleEditMode = (enable) => {
82+
setInstructionsText(enable ? localizedText.editModeDisable : '');
83+
};
84+
85+
const onFocusCell = (cell, event) => {
86+
const { row, col } = cell;
87+
const key = event.which || event.keyCode;
88+
const navigatedHorizontally = (key === keycode.codes.left || key === keycode.codes.right) && col > 0;
89+
const navigatedVertically = (key === keycode.codes.up || key === keycode.codes.down) && row > 0;
90+
if (gridManager.current?.editMode) {
91+
setInstructionsText(localizedText.editModeDisable);
92+
} else {
93+
let newInstructionsText = '';
94+
if (navigatedVertically) {
95+
newInstructionsText += `${localizedText.row} ${row} `;
96+
} else if (navigatedHorizontally) {
97+
newInstructionsText += `${localizedText.column} ${col} ${headers[col]} `;
98+
} else {
99+
newInstructionsText += `${localizedText.arrowKeys} `;
100+
}
101+
if (gridManager.current?.isEditableCell(cell)) {
102+
newInstructionsText += localizedText.editModeEnable;
103+
}
104+
setInstructionsText(newInstructionsText);
105+
}
106+
};
107+
47108
let checkboxHeader;
48-
let displayHeaders = headers;
109+
let displayHeaders = [...headers];
49110

50111
if (richTable) {
51112
checkboxHeader = <th className={tableCheckboxClasses}>{headers[0]}</th>;
52-
displayHeaders = headers.splice(1, headers.length);
113+
displayHeaders = displayHeaders.splice(1, headers.length);
53114
}
54115

55116
return (
56-
<table {...props} className={tableClasses}
57-
ref={ref}>
117+
<table {...props} aria-describedby={captionId}
118+
className={tableClasses}
119+
ref={tableRef}
120+
role={keyboardNavigation ? 'grid' : 'table'}>
121+
<caption aria-live='polite' className='fd-table__caption'
122+
id={captionId}>
123+
{instructionsText}
124+
</caption>
58125
<thead className={tableHeaderClasses} {...tableHeaderProps}>
59126
<tr className={tableHeaderRowClasses} {...tableHeaderRowProps}>
60127
{richTable && checkboxHeader}
@@ -66,7 +133,8 @@ const Table = React.forwardRef(({ headers, tableData, className, tableBodyClassN
66133
<tbody className={tableBodyClasses} {...tableBodyProps}>
67134
{tableData.map((row, index) => {
68135
let rowProps, checkboxCell;
69-
let displayRows = row.rowData;
136+
let displayCells = [...row.rowData];
137+
70138
if (tableBodyRowProps) {
71139
rowProps = (typeof tableBodyRowProps === 'function'
72140
? tableBodyRowProps(row, index)
@@ -80,7 +148,7 @@ const Table = React.forwardRef(({ headers, tableData, className, tableBodyClassN
80148
{row.rowData[0]}
81149
</td>
82150
);
83-
displayRows = row.rowData.splice(1, row.rowData.length);
151+
displayCells = displayCells.splice(1, row.rowData.length);
84152
}
85153

86154
return (
@@ -90,8 +158,11 @@ const Table = React.forwardRef(({ headers, tableData, className, tableBodyClassN
90158
aria-selected={row?.rowData[0]?.props?.checked}
91159
key={index}>
92160
{richTable && checkboxCell}
93-
{displayRows.map((rowData, cellIndex) => {
94-
return <td className={tableCellClasses} key={cellIndex}>{rowData}</td>;
161+
{displayCells.map((cellData, cellIndex) => {
162+
if (cellData.type?.propTypes?.compact) {
163+
cellData = React.cloneElement(cellData, { compact: compact || condensed });
164+
}
165+
return <td className={tableCellClasses} key={cellIndex}>{cellData}</td>;
95166
})}
96167
</tr>
97168
);
@@ -106,14 +177,33 @@ Table.displayName = 'Table';
106177
Table.propTypes = {
107178
/** Array of localized text strings for the column headers */
108179
headers: PropTypes.array.isRequired,
109-
/** Array of objects that contain one property: `rowData` (an array of strings containing data for each column in the row) */
180+
/** Array of objects that contain one property: `rowData` (an array of nodes containing data for each column in the row) */
110181
tableData: PropTypes.arrayOf(
111182
PropTypes.shape({
112183
rowData: PropTypes.array
113184
}).isRequired
114185
).isRequired,
115186
/** CSS class(es) to add to the element */
116187
className: PropTypes.string,
188+
/** Set to **true** to enable compact mode */
189+
compact: PropTypes.bool,
190+
/** Set to **true** to enable condensed mode */
191+
condensed: PropTypes.bool,
192+
/** Determines the type of keyboard navigation for the table. Set to `'cell'` for cell-level navigation or `'row'` for row-level navigation */
193+
keyboardNavigation: PropTypes.oneOf(['none', 'cell', 'row']),
194+
/** Localized text to be updated based on location/language */
195+
localizedText: CustomPropTypes.i18n({
196+
/** Localized string informing screen reader users the table can be navigated by arrow keys */
197+
arrowKeys: PropTypes.string,
198+
/** Localized string informing screen reader users the current cell can be edited */
199+
editModeEnable: PropTypes.string,
200+
/** Localized string informing screen reader users how to return to cell navigation */
201+
editModeDisable: PropTypes.string,
202+
/** Localized string for 'row' */
203+
row: PropTypes.string,
204+
/** Localized string for 'column' */
205+
column: PropTypes.string
206+
}),
117207
/** Set to **true** if Table contains checkboxes */
118208
richTable: PropTypes.bool,
119209
/** Additional classes to be added to the `<tbody>` element */
@@ -142,4 +232,15 @@ Table.propTypes = {
142232
tableRowClassName: PropTypes.string
143233
};
144234

235+
Table.defaultProps = {
236+
keyboardNavigation: 'none',
237+
localizedText: {
238+
arrowKeys: 'Use arrow keys to navigate between cells',
239+
editModeEnable: 'Press Enter to edit this cell',
240+
editModeDisable: 'Press Escape to return to cell navigation',
241+
row: 'row',
242+
column: 'column'
243+
}
244+
};
245+
145246
export default Table;

0 commit comments

Comments
 (0)