diff --git a/docs/14_0_0/components/datatable.md b/docs/14_0_0/components/datatable.md
index 00297f35c5..5ff2a986d3 100644
--- a/docs/14_0_0/components/datatable.md
+++ b/docs/14_0_0/components/datatable.md
@@ -23,6 +23,7 @@ DataTable displays data in tabular format.
| ariaRowLabel | null | String | Label to read by screen readers on checkbox selection.
| binding | null | Object | An el expression that maps to a server side UIComponent instance in a backing bean
| cellEditMode | eager | String | Defines the cell edit behavior, valid values are "eager" (default) and "lazy".
+| cellNavigation | true | Boolean | Enables cell navigation with the keyboard for WCAG and screen reader compliance.
| cellSeparator | null | String | Separator text to use in output mode of editable cells with multiple components.
| clientCache | false | Boolean | Caches the next page asynchronously, default is false.
| currentPageReportTemplate | null | String | Template of the currentPageReport UI.
diff --git a/docs/14_0_0/gettingstarted/whatsnew.md b/docs/14_0_0/gettingstarted/whatsnew.md
index fdfb046acb..d2fb5e1fad 100644
--- a/docs/14_0_0/gettingstarted/whatsnew.md
+++ b/docs/14_0_0/gettingstarted/whatsnew.md
@@ -48,6 +48,7 @@ Look into [migration guide](https://primefaces.github.io/primefaces/14_0_0/#/../
* JPALazyDataModel now supports post load data enricher with `JPALazyDataModel.Builder#resultEnricher()` .
* Added `filterPlaceholder` for `Column` and `Columns`
* Added `rowData` to `CellEditEvent` which contains the entire row from the cell being edited.
+ * Added `cellNavigation` property which defaults to true to enable WCAG keyboard navigation of table cells.
* Dashboard
* Added `var` to allow dynamic panels in `DashboardWidget.setValue(obj)` per panel
diff --git a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java
index c67d959597..f8f071b876 100644
--- a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java
+++ b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableBase.java
@@ -44,6 +44,7 @@ public enum PropertyKeys {
ariaRowLabel,
caseSensitiveSort,
cellEditMode,
+ cellNavigation,
cellSeparator,
clientCache,
dataLocale,
@@ -439,7 +440,7 @@ public void setRowExpandMode(String rowExpandMode) {
getStateHelper().put(PropertyKeys.rowExpandMode, rowExpandMode);
}
- public Object getDataLocale() {
+ @Override public Object getDataLocale() {
return getStateHelper().eval(PropertyKeys.dataLocale, null);
}
@@ -768,4 +769,11 @@ public void setSelectAllFilteredOnly(boolean selectAllFilteredOnly) {
getStateHelper().put(PropertyKeys.selectAllFilteredOnly, selectAllFilteredOnly);
}
+ public boolean isCellNavigation() {
+ return (Boolean) getStateHelper().eval(PropertyKeys.cellNavigation, true);
+ }
+
+ public void setCellNavigation(boolean cellNavigation) {
+ getStateHelper().put(PropertyKeys.cellNavigation, cellNavigation);
+ }
}
diff --git a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java
index 8d992f975c..6efcc8dedf 100644
--- a/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java
+++ b/primefaces/src/main/java/org/primefaces/component/datatable/DataTableRenderer.java
@@ -249,6 +249,7 @@ protected void encodeScript(FacesContext context, DataTable table) throws IOExce
.attr("rowHover", table.isRowHover(), false)
.attr("clientCache", table.isClientCache(), false)
.attr("multiViewState", table.isMultiViewState(), false)
+ .attr("cellNavigation", table.isCellNavigation(), true)
.attr("partialUpdate", table.isPartialUpdate(), true)
.nativeAttr("groupColumnIndexes", table.getGroupedColumnIndexes(), null)
.callback("onRowClick", "function(row)", table.getOnRowClick());
diff --git a/primefaces/src/main/resources/META-INF/primefaces.taglib.xml b/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
index 999b066c40..da62752edb 100644
--- a/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
+++ b/primefaces/src/main/resources/META-INF/primefaces.taglib.xml
@@ -8956,6 +8956,14 @@
false
java.lang.String
+
+
+
+
+ cellNavigation
+ false
+ java.lang.Boolean
+
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.css b/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.css
index 9919b6f745..9a769be0a2 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.css
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.css
@@ -157,7 +157,17 @@
top: 0px;
left: 0px;
position: absolute;
-}
+}
+
+/** Accessibility **/
+.ui-datatable-data td[role="gridcell"]:focus,
+.ui-datatable-data td[role="gridcell"] *:focus,
+.ui-datatable-data td[role="grid"] [tabindex="0"]:focus {
+ outline: var(--primary-color, #90caf9);
+ outline-style: solid;
+ outline-width: 2px;
+ border: none;
+}
/* InCell Editing */
.ui-datatable .ui-cell-editor-input {
diff --git a/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.js b/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.js
index 1904c857d3..c7510b9bd6 100644
--- a/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.js
+++ b/primefaces/src/main/resources/META-INF/resources/primefaces/datatable/datatable.js
@@ -240,6 +240,9 @@ PrimeFaces.widget.DataTable = PrimeFaces.widget.DeferredWidget.extend({
if(this.cfg.selectionMode) {
this.setupSelection();
}
+ else {
+ this.cfg.selectionRowMode = this.cfg.selectionRowMode || 'none';
+ }
if(this.cfg.filter) {
this.setupFiltering();
@@ -317,6 +320,15 @@ PrimeFaces.widget.DataTable = PrimeFaces.widget.DeferredWidget.extend({
if(this.cfg.reflow) {
this.jq.css('visibility', 'visible');
}
+
+ this.cfg.cellNavigation = this.cfg.cellNavigation === undefined ? true : this.cfg.cellNavigation;
+ if (this.cfg.editMode === 'cell' || this.cfg.selectionRowMode !== 'none') {
+ // do not allow when editing or row selection enabled
+ this.cfg.cellNavigation = false;
+ }
+ if (this.cfg.cellNavigation) {
+ this.setupNavigableCells();
+ }
},
/**
@@ -382,6 +394,10 @@ PrimeFaces.widget.DataTable = PrimeFaces.widget.DeferredWidget.extend({
if(this.cfg.expansion) {
this.initRowExpansion();
}
+
+ if (this.cfg.cellNavigation) {
+ this.setupNavigableCells();
+ }
},
/**
@@ -794,6 +810,141 @@ PrimeFaces.widget.DataTable = PrimeFaces.widget.DeferredWidget.extend({
this.bindRowHover(selector);
}
},
+
+ /**
+ * Sets up WCAG keyboard navigation of cells.
+ * @private
+ */
+ setupNavigableCells: function() {
+ var $this = this;
+ var pageRows = this.cfg.paginator && this.cfg.paginator.rows ? this.cfg.paginator.rows : 1000;
+
+ // helper function to set the current and next cell focus
+ function makeFocusable(e, cell, nextCell) {
+ if (cell && cell.length) {
+ cell.attr("tabindex", "-1");
+ }
+
+ if (nextCell && nextCell.length) {
+ nextCell.attr("tabindex", "0").trigger("focus");
+ e.preventDefault();
+ }
+ }
+
+ // helper function to reset the state of the whole table
+ function resetFocusable(resetFirstCell) {
+ var selector = resetFirstCell ? "td" : 'td[tabindex="0"]';
+ // default all cells to not focusable
+ var focusableCells = $this.getTbody().find(selector);
+ focusableCells.attr("tabindex", "-1");
+
+ if (resetFirstCell) {
+ // the very first cell should be focusable
+ focusableCells.first().attr("tabindex", "0");
+ }
+ }
+
+ // default all cells to not focusable except the very first cell
+ resetFocusable(true);
+
+ // on click should make it focusable
+ this.getTbody().find("td")
+ .on("click.focuscell", function(e) {
+ if ($(e.target).is(":input")) {
+ return;
+ }
+ resetFocusable(false);
+
+ makeFocusable(e, null, $(this));
+ })
+ .on("keydown.focuscell", function(e) {
+ if ($(e.target).is(":input")) {
+ return;
+ }
+ var cell = $(this);
+
+ switch (e.code) {
+ case "ArrowLeft":
+ var prevCell = $this.isRTL ? cell.next('[tabindex="-1"]') : cell.prev('[tabindex="-1"]');
+ makeFocusable(e, cell, prevCell);
+ break;
+ case "ArrowRight":
+ var nextCell = $this.isRTL ? cell.prev('[tabindex="-1"]') : cell.next('[tabindex="-1"]');
+ makeFocusable(e, cell, nextCell);
+ break;
+ case "ArrowDown":
+ var nextCell = cell.closest("tr[data-ri]").next().find('td[tabindex="-1"]').eq(cell.index());
+ makeFocusable(e, cell, nextCell);
+ break;
+ case "ArrowUp":
+ var prevCell = cell.closest("tr[data-ri]").prev().find('td[tabindex="-1"]').eq(cell.index());
+ makeFocusable(e, cell, prevCell);
+ break;
+ case "Home":
+ var prevCell = cell.prevAll('[tabindex="-1"]').last();
+ if (e.ctrlKey) {
+ prevCell = cell.closest("tr[data-ri]").prevAll().last().find('td[tabindex="-1"]').first();
+ }
+ makeFocusable(e, cell, prevCell);
+ break;
+ case "End":
+ var nextCell = cell.nextAll('[tabindex="-1"]').last();
+ if (e.ctrlKey) {
+ nextCell = cell.closest("tr[data-ri]").nextAll().last().find('td[tabindex="-1"]').last();
+ }
+ makeFocusable(e, cell, nextCell);
+ break;
+ case "PageUp":
+ // Select the current record
+ var currentRow = cell.closest("tr[data-ri]");
+ var prevCell = null;
+
+ // Navigate back 1 page records
+ for (var i = 0; i < pageRows; i++) {
+ currentRow = currentRow.prev("tr[data-ri]");
+ if (currentRow.length) {
+ prevCell = currentRow.find('td[tabindex="-1"]').eq(cell.index());
+ } else {
+ break;
+ }
+ }
+ makeFocusable(e, cell, prevCell);
+ break;
+ case "PageDown":
+ // Select the current row
+ var currentRow = cell.closest("tr[data-ri]");
+ var nextCell = null;
+
+ // Navigate forward 1 page rows
+ for (var i = 0; i < pageRows; i++) {
+ currentRow = currentRow.next("tr[data-ri]");
+ if (currentRow.length) {
+ nextCell = currentRow.find('td[tabindex="-1"]').eq(cell.index());
+ } else {
+ break;
+ }
+ }
+ makeFocusable(e, cell, nextCell);
+ break;
+ case "Space":
+ case "Enter":
+ case "NumpadEnter":
+ // Find the first child element with a click event bound
+ var $clickable = cell.find(':button:enabled, :input:enabled, a').first();
+ if ($clickable.length) {
+ $clickable.trigger('click');
+ e.stopPropagation();
+ e.preventDefault();
+ }
+ else {
+ cell.trigger('click');
+ }
+ break;
+ default:
+ break;
+ }
+ });
+ },
/**
* Sets up the DataTable and adds all event listener required for selecting rows.