Skip to content

Commit

Permalink
Fix primefaces#11783: 14.0.1 Datatable keyboard/screen reader support…
Browse files Browse the repository at this point in the history
… for cells
  • Loading branch information
melloware committed May 22, 2024
1 parent 26f676b commit 4ca0f88
Show file tree
Hide file tree
Showing 7 changed files with 182 additions and 2 deletions.
1 change: 1 addition & 0 deletions docs/14_0_0/components/datatable.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
1 change: 1 addition & 0 deletions docs/14_0_0/gettingstarted/whatsnew.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ public enum PropertyKeys {
ariaRowLabel,
caseSensitiveSort,
cellEditMode,
cellNavigation,
cellSeparator,
clientCache,
dataLocale,
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand Down
8 changes: 8 additions & 0 deletions primefaces/src/main/resources/META-INF/primefaces.taglib.xml
Original file line number Diff line number Diff line change
Expand Up @@ -8956,6 +8956,14 @@
<required>false</required>
<type>java.lang.String</type>
</attribute>
<attribute>
<description>
<![CDATA[Enables cell navigation with the keyboard for WCAG and screen reader compliance. Default is true.]]>
</description>
<name>cellNavigation</name>
<required>false</required>
<type>java.lang.Boolean</type>
</attribute>
<attribute>
<description>
<![CDATA[Loads data on demand as the scrollbar gets close to the bottom. Default is false.]]>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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();
}
},

/**
Expand Down Expand Up @@ -382,6 +394,10 @@ PrimeFaces.widget.DataTable = PrimeFaces.widget.DeferredWidget.extend({
if(this.cfg.expansion) {
this.initRowExpansion();
}

if (this.cfg.cellNavigation) {
this.setupNavigableCells();
}
},

/**
Expand Down Expand Up @@ -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.
Expand Down

0 comments on commit 4ca0f88

Please sign in to comment.