-
Notifications
You must be signed in to change notification settings - Fork 72
Compact interactive list keyboard navigation #1919
Conversation
Co-authored-by: Saad Adnan <[email protected]>
…om/cerner/terra-framework into compact_interactive_list_keyboard_navigation
columnMinimumWidth: PropTypes.string, | ||
|
||
/** | ||
* Columns maximum width should be a valid css string in value in px, em, or rem units.. | ||
* Columns maximum width should be a valid css string in value in px, em, or rem units. | ||
*/ | ||
columnMaximumWidth: PropTypes.string, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Wouldn't the minimum and maximum widths be part of the columnShape? I would expect to be able to control the values for each column.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are 2 ways to control column max and min width:
Option 1: specify min and max width for a specific column as a part of column shape prop:
terra-framework/packages/terra-compact-interactive-list/src/proptypes/columnShape.js
Line 27 in 466e8c8
minimumWidth: PropTypes.string, |
Option 2: (the one you are looking at)
columnMaximumWidth
and columnMinimumWidth
prop of the whole component, this way they'll be applied to each column.
Option 1 takes precedence on Option 2, so if there is columnMaximumWidth
set to the whole component and hence applied to all columns, but then some columns have minimumWidth
(or maximumWidth
) props set for these columns directly, the last ones would be applied to those columns.
@mjpalazzo @adoroshk I wouldn't want to give overly verbose instructions detailing every possible interaction, especially since that also needs to be available to non screen reader users, but something like "Use arrow keys to navigate and space or enter to activate a cell" would probably be helpful. |
const onFocus = (event) => { | ||
if (!event.currentTarget.contains(event.relatedTarget)) { | ||
// Not triggered when swapping focus between children | ||
focusCell(focusedCell.current); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What happens if the list was updated while focus was lost. You will end up with a similar exception that we had to fix for the WorklistDataGrid.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. In this commit I changed the focused cell data to row and column ids instead of indexes: 396ed59
export const getFocusableElements = (parentElement) => [...parentElement.querySelectorAll(`${focusableElementSelector}`)].filter( | ||
element => !element.hasAttribute('disabled') | ||
&& !element.getAttribute('aria-hidden') | ||
&& window.getComputedStyle(element).visibility !== 'hidden', | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We have a similar method in focusManagement.js in the Table component. I would think we would not want to duplicate it all. Same comment for the other methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually it's a bit different. I have simplified this one a bit as the one you are referring to checks for offsets for cases when components are not hidden but rather moved outside of the container to not be visible for sighted user. It was messing with jest enzyme tests and as I don't expect the cell having that king of "hidden" elements, I simplified it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You should not need to change it. You just need to update your jest tests appropriately. If you check the other jest tests, there is code to handle providing an offset value. The logic should work for your component.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@cm9361 huge thank you for the help with tests to accommodate the full original logic from terra-table. I fixed the tests and currently added getFocusableElements is the original one that is implemented in terra-table.
In order to reuse the same logic rather than copy-paste it, I added the export for the getFocusableElements
method from the terra-table
. Once the terra-table is released, I will remove copied getFocusableElements method from the compact interactive list and attach the one exported from the terra-table. Would that work?
The commit: cdbf56a
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can use a relative path instead of having to add a package dependency. That logic was released in terra-table quite a while ago though, if I am not mistaken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct, imported from terra-table: e7343db
/** | ||
* The cell's column position in the table. This is zero based. | ||
*/ | ||
columnIndex: PropTypes.number, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shouldn't the cell also have a column id?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
UPD: there is a full column info passed a s a column prop (needed for cell max/min width, etc). It also has a column id.
… compact_interactive_list_keyboard_navigation
const handleKeyDown = (event) => { | ||
// does not need setFocusedCell call as the cell focused by key is tracked in CompactInteractiveList component | ||
const key = event.keyCode; | ||
if (key === KeyCode.KEY_SPACE && isSelectableCell) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So a cell cannot be selected if it doesn't have interactable elements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per @eawww at that moment (unless it changes in the future) the cell can NOT be selected If there is an interactable element in it (and there can be only one intreactive element per cell).
return focusedCell; | ||
} | ||
// focus should go to the next semantic row unless the last row | ||
return { row: row + 1 < rowsLength ? row + 1 : row, cell }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You probably could simplify this by using the Math.min function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure I got how to do that. An example will be appreciated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you @adoroshk! It works fantastically.
One last request
Could you add to the prop description for numberOfColumns
: "Number of rows is calculated as the number of items divided by the number of columns, rounded up." or if there's a more accurate wording, that'd be cool.
@eawww added the description here: terra-framework/packages/terra-compact-interactive-list/src/CompactInteractiveList.jsx Line 73 in 396ed59
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a nit and a question, but otherwise looks good.
* @param {number} rowsLength - a total number of seamntic rows in the list. | ||
* @returns - a { row, cell } indexes pair to focus on. | ||
*/ | ||
export const handleDownKey = (focusedCell, numberOfColumns, flowHorizontally, rowsLength) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: I'm wondering if there was a way to not have to delegate all the directional keys to a unique function. I see that in each handleXKey
function, we're re-stating a lot of the same logic and params across each function. In the DataGrid, we generally were able to keep all the directional handles in the same function.
Up to you on how/when/if to address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I understand your concern, I'll look into shared logic between these methods and see if it can be improved in my next PR, this one gets too complex to monitor the changes.
event.preventDefault(); | ||
break; | ||
case KeyCode.KEY_LEFT: { | ||
moveFocusTo = handleLeftKey(event, moveFocusTo, numberOfColumns, flowHorizontally, columns.length, rows.length); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need to event.preventDefault()
on the other navigation actions? I would think so as all of these are related to navigation.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch on preventDefault
inconsistency. Actually, we don't need the preventDefault
on Up and Down keys, as there is one preventDefault for all cases after the switch. Escape and Tab bave return at the end, so they need their own preventDefaults, but arrow keys have break
, not return
and go to the end of the method to reach the preventDefault
there and I just missed that. I removed extra in this commit: a6bcd2f
Summary
This PR adds keyboard navigation to compact interactive list component.
What was changed:
Following functionality has been added:
The list is a single roving tab stop.
Right/Left Arrows navigate across visual rows. When focus hits the end of the visual row navigation stops.
Up/Down Arrows navigate across visual columns. Navigating down from the bottom of a visual column takes you to the corresponding cell at the top of the next visual column and vice versa (except for the last and first items).
Moves to the first cell in the focused visual row
Moves to the last cell in the focused visual row
Moves to the first cell in the first item (top left)
Moves to the last cell in the last item (bottom item in rightmost visual column)
If cell has an interactive element (only one per cell allowed), focus is set to that element, not the cell (only one interactive element per cell allowed) and the arrow keys behavior is default to that element (NOTE: currently it causes problems with input elements that won't allow to navigate away from that with keyboard, a decision needs to be maid by UX team on what is the best way around that).
Cells are select-able by Space key (it triggers onClick for cell) unless the cell has an interactable element within, in that case its not selectable neither via mouse click nor per space key.
Testing
Note: at that moment tests are work in progress.
This change was tested using:
Reviews
In addition to engineering reviews, this PR needs:
Additional Details
This PR resolves:
UXPLATFORM-9788