Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve the reverse navigation order of focusable elements within KTable cells #840

Open
wants to merge 18 commits into
base: develop
Choose a base branch
from
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
225 changes: 109 additions & 116 deletions lib/KTable/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -294,156 +294,149 @@
* - header highlight
*/
handleKeydown(event, rowIndex, colIndex) {
const key = event.key;
switch (event.key) {
case 'ArrowUp':
case 'ArrowDown':
case 'ArrowLeft':
case 'ArrowRight':
this.handleArrowKeys(event.key, rowIndex, colIndex);
break;
case 'Enter':
this.handleEnterKey(rowIndex, colIndex);
break;
case 'Tab':
this.handleTabKey(event, rowIndex, colIndex);
break;
default:
break;
}
},

handleArrowKeys(key, rowIndex, colIndex) {
const totalRows = this.rows.length;
const totalCols = this.headers.length;

const lastRowIndex = totalRows - 1;
const lastColIndex = totalCols - 1;
let nextRowIndex = rowIndex;
let nextColIndex = colIndex;

switch (key) {
case 'ArrowUp':
if (rowIndex === -1) {
nextRowIndex = totalRows - 1;
} else {
nextRowIndex = rowIndex - 1;
}
nextRowIndex = rowIndex === -1 ? lastRowIndex : rowIndex - 1;
break;
case 'ArrowDown':
if (rowIndex === -1) {
nextRowIndex = 0;
} else if (rowIndex === totalRows - 1) {
nextRowIndex = -1;
} else {
nextRowIndex = (rowIndex + 1) % totalRows;
}
nextRowIndex = rowIndex === -1 ? 0 : rowIndex === lastRowIndex ? -1 : rowIndex + 1;
break;
case 'ArrowLeft':
if (rowIndex === -1) {
if (colIndex > 0) {
nextColIndex = colIndex - 1;
} else {
nextColIndex = totalCols - 1;
nextRowIndex = totalRows - 1;
}
} else if (colIndex > 0) {
nextColIndex = colIndex - 1;
nextColIndex = colIndex > 0 ? colIndex - 1 : lastColIndex;
nextRowIndex = colIndex === 0 ? lastRowIndex : -1;
} else {
nextColIndex = totalCols - 1;
nextRowIndex = rowIndex > 0 ? rowIndex - 1 : -1;
nextColIndex = colIndex > 0 ? colIndex - 1 : lastColIndex;
nextRowIndex = colIndex === 0 ? (rowIndex > 0 ? rowIndex - 1 : -1) : rowIndex;
}
break;
case 'ArrowRight':
if (colIndex === totalCols - 1) {
if (rowIndex === totalRows - 1) {
nextColIndex = 0;
nextRowIndex = -1;
} else {
nextColIndex = 0;
nextRowIndex = rowIndex + 1;
}
if (colIndex === lastColIndex) {
nextColIndex = 0;
nextRowIndex = rowIndex === lastRowIndex ? -1 : rowIndex + 1;
} else {
nextColIndex = colIndex + 1;
}
break;
case 'Enter':
if (rowIndex === -1 && this.sortable) {
this.handleSort(colIndex);
}
break;
case 'Tab': {
// Identify all focusable elements inside the current cell
const currentCell = this.getCell(rowIndex, colIndex);
}
this.updateFocusState(nextRowIndex, nextColIndex);
event.preventDefault();
},

// Collect focusable elements using native DOM methods
const focusableElements = [];
handleEnterKey(rowIndex, colIndex) {
if (rowIndex === -1 && this.sortable) {
this.handleSort(colIndex);
}
},

if (currentCell) {
const buttons = currentCell.getElementsByTagName('button');
const links = currentCell.getElementsByTagName('a');
const inputs = currentCell.getElementsByTagName('input');
const selects = currentCell.getElementsByTagName('select');
const textareas = currentCell.getElementsByTagName('textarea');
handleTabKey(event, rowIndex, colIndex) {
const totalRows = this.rows.length;
const totalCols = this.headers.length;
let nextRowIndex = rowIndex;
let nextColIndex = colIndex;

focusableElements.push(...buttons, ...links, ...inputs, ...selects, ...textareas);
}
const currentCell = this.getCell(rowIndex, colIndex);
const focusableElements = this.getFocusableElements(currentCell);
const focusedElement = document.activeElement;

const focusedElementIndex = focusableElements.indexOf(document.activeElement);
if (focusableElements.length > 0) {
if (!event.shiftKey) {
// if navigating between more focusable elements within the cell
if (focusedElementIndex < focusableElements.length - 1) {
focusableElements[focusedElementIndex + 1].focus();
event.preventDefault();
return;
} else {
if (colIndex < totalCols - 1) {
nextColIndex = colIndex + 1;
} else if (rowIndex < totalRows - 1) {
nextColIndex = 0;
nextRowIndex = rowIndex + 1;
} else {
// Allow default behavior when reaching the last cell
return;
}
}
} else {
if (focusedElementIndex < focusableElements.length - 1) {
// if navigating between more focusable elements within the cell
focusableElements[focusedElementIndex + 1].focus();
event.preventDefault();
return;
} else {
if (colIndex > 0) {
nextColIndex = colIndex - 1;
} else if (rowIndex > 0) {
nextColIndex = totalCols - 1;
nextRowIndex = rowIndex - 1;
} else {
// Allow default behavior when reaching the first cell
return;
}
}
}
// Include the cell itself as the first focusable element
const cellAndFocusableElements = [currentCell, ...focusableElements];
const focusedIndex = cellAndFocusableElements.indexOf(focusedElement);

if (!event.shiftKey) {
// Tab key navigation
if (focusedIndex < cellAndFocusableElements.length - 1) {
// Move to the next focusable element within the cell
cellAndFocusableElements[focusedIndex + 1].focus();
event.preventDefault();
return;
} else {
// Move to the first focusable element of the next cell
if (colIndex < totalCols - 1) {
nextColIndex = colIndex + 1;
} else if (rowIndex < totalRows - 1) {
nextColIndex = 0;
nextRowIndex = rowIndex + 1;
} else {
if (!event.shiftKey) {
if (colIndex < totalCols - 1) {
nextColIndex = colIndex + 1;
} else if (rowIndex < totalRows - 1) {
nextColIndex = 0;
nextRowIndex = rowIndex + 1;
} else {
// Allow default behavior when reaching the last cell
return;
}
} else {
if (colIndex > 0) {
nextColIndex = colIndex - 1;
} else if (rowIndex > 0) {
nextColIndex = totalCols - 1;
nextRowIndex = rowIndex - 1;
} else {
// Allow default behavior when reaching the first cell
return;
}
}
return; // Allow default Tab behavior when reaching the end
}

break;
const nextCell = this.getCell(nextRowIndex, nextColIndex);
const nextFocusableElements = this.getFocusableElements(nextCell);
const nextCellAndFocusableElements = [nextCell, ...nextFocusableElements];
nextCellAndFocusableElements[0].focus();
this.updateFocusState(nextRowIndex, nextColIndex);
event.preventDefault();
}

default:
} else {
// Shift+Tab key navigation
if (focusedIndex > 0) {
// Move to the previous focusable element within the cell
cellAndFocusableElements[focusedIndex - 1].focus();
event.preventDefault();
return;
} else {
// Move to the last focusable element of the previous cell
if (colIndex > 0) {
nextColIndex = colIndex - 1;
} else if (rowIndex > 0) {
nextColIndex = totalCols - 1;
nextRowIndex = rowIndex - 1;
} else {
return; // Allow default Shift+Tab behavior when at the beginning
}
const prevCell = this.getCell(nextRowIndex, nextColIndex);
const prevFocusableElements = this.getFocusableElements(prevCell);
const prevCellAndFocusableElements = [prevCell, ...prevFocusableElements];
prevCellAndFocusableElements[prevCellAndFocusableElements.length - 1].focus();
this.updateFocusState(nextRowIndex, nextColIndex, false);
event.preventDefault();
}
}

this.focusCell(nextRowIndex, nextColIndex);
},
updateFocusState(nextRowIndex, nextColIndex, shouldFocusCell = true) {
this.focusedRowIndex = nextRowIndex === -1 ? null : nextRowIndex;
this.focusedColIndex = nextColIndex;

this.highlightHeader(nextColIndex);
// Focus the cell only if it is necessary
if (shouldFocusCell) {
this.focusCell(nextRowIndex, nextColIndex);
}
},

event.preventDefault();
getFocusableElements(cell) {
if (!cell) return [];
const focusableSelectors = ['button', 'a', 'input', 'select', 'textarea'];
return focusableSelectors.flatMap(selector =>
Array.from(cell.getElementsByTagName(selector))
);
},

getCell(rowIndex, colIndex) {
if (rowIndex === -1) {
return this.$refs[`header-${colIndex}`][0];
Expand Down