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

Enhancement: DataTable accessibility #5839

Merged
merged 5 commits into from
Feb 1, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
9 changes: 7 additions & 2 deletions components/lib/datatable/BodyCell.js
Original file line number Diff line number Diff line change
Expand Up @@ -546,7 +546,9 @@ export const BodyCell = React.memo((props) => {
field: field
});
const content = ObjectUtils.getJSXElement(getVirtualScrollerOption('loadingTemplate'), options);
const bodyCellProps = mergeProps(getColumnPTOptions('bodyCell'));
const bodyCellProps = mergeProps(getColumnPTOptions('bodyCell'), {
role: 'cell'
});

return <td {...bodyCellProps}>{content}</td>;
};
Expand Down Expand Up @@ -700,6 +702,7 @@ export const BodyCell = React.memo((props) => {
onClick: rowEditorProps.onSaveClick,
className: rowEditorProps.saveClassName,
tabIndex: props.tabIndex,
'aria-label': ariaLabel('saveEdit'),
'data-p-row-editor-save': true
},
getColumnPTOptions('rowEditorSaveButton')
Expand All @@ -712,7 +715,8 @@ export const BodyCell = React.memo((props) => {
'aria-label': ariaLabel('cancelEdit'),
onClick: rowEditorProps.onCancelClick,
className: rowEditorProps.cancelClassName,
tabIndex: props.tabIndex
tabIndex: props.tabIndex,
'aria-label': ariaLabel('cancelEdit')
},
getColumnPTOptions('rowEditorCancelButton')
);
Expand Down Expand Up @@ -744,6 +748,7 @@ export const BodyCell = React.memo((props) => {
onClick: rowEditorProps.onInitClick,
className: rowEditorProps.initClassName,
tabIndex: props.tabIndex,
'aria-label': ariaLabel('editRow'),
'data-p-row-editor-init': true
},
getColumnPTOptions('rowEditorInitButton')
Expand Down
128 changes: 105 additions & 23 deletions components/lib/datatable/BodyRow.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,12 @@ export const BodyRow = React.memo((props) => {
}
};

const findFirstSelectableRow = (row) => {
const firstRow = DomHandler.findSingle(row.parentNode, 'tr[data-p-selectable-row]');

return firstRow ? firstRow : null;
};

const findNextSelectableRow = (row) => {
const nextRow = row.nextElementSibling;

Expand All @@ -73,6 +79,12 @@ export const BodyRow = React.memo((props) => {
return prevRow ? (DomHandler.getAttribute(prevRow, 'data-p-selectable-row') === true ? prevRow : findPrevSelectableRow(prevRow)) : null;
};

const findLastSelectableRow = (row) => {
const lastRow = DomHandler.findSingle(row.parentNode, 'tr[data-p-selectable-row]:last-child');

return lastRow ? lastRow : null;
};

const shouldRenderBodyCell = (value, column, i) => {
if (getColumnProp(column, 'hidden')) {
return false;
Expand Down Expand Up @@ -152,44 +164,37 @@ export const BodyRow = React.memo((props) => {
switch (event.which) {
//down arrow
case 40:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should switch those to event.code like 'ArrowDown' etc. the old event.which is deprecated

let nextRow = findNextSelectableRow(row);

if (nextRow) {
changeTabIndex(row, nextRow);
nextRow.focus();
}

event.preventDefault();
onArrowDownKey(row, event);
break;

//up arrow
case 38:
let prevRow = findPrevSelectableRow(row);
onArrowUpKey(row, event);
break;

if (prevRow) {
changeTabIndex(row, prevRow);
prevRow.focus();
}
//home
case 36:
onHomeKey(row, event);
break;

event.preventDefault();
//end
case 35:
onEndKey(row, event);
break;

//enter
case 13: // @deprecated
if (!DomHandler.isClickable(target)) {
onClick(event);
event.preventDefault();
}

onEnterKey(row, event, target);
break;

//space
case 32:
if (!DomHandler.isClickable(target) && !target.readOnly) {
onClick(event);
event.preventDefault();
}
onSpaceKey(row, event, target);
break;

//tab
case 9:
onTabKey(row, event);
break;

default:
Expand All @@ -199,6 +204,82 @@ export const BodyRow = React.memo((props) => {
}
};

const onArrowDownKey = (row, event) => {
let nextRow = findNextSelectableRow(row);

if (nextRow) {
changeTabIndex(row, nextRow);
nextRow.focus();
}

event.preventDefault();
};

const onArrowUpKey = (row, event) => {
let prevRow = findPrevSelectableRow(row);

if (prevRow) {
changeTabIndex(row, prevRow);
prevRow.focus();
}

event.preventDefault();
};

const onHomeKey = (row, event) => {
const firstRow = findFirstSelectableRow(row);

if (firstRow) {
changeTabIndex(row, firstRow);
firstRow.focus();
}

event.preventDefault();
};

const onEndKey = (row, event) => {
const lastRow = findLastSelectableRow(row);

if (lastRow) {
changeTabIndex(row, lastRow);
lastRow.focus();
}

event.preventDefault();
};

const onEnterKey = (row, event, target) => {
if (!DomHandler.isClickable(target)) {
onClick(event);
event.preventDefault();
}
};

const onSpaceKey = (row, event, target) => {
if (!DomHandler.isClickable(target) && !target.readOnly) {
onClick(event);
event.preventDefault();
}
};

const onTabKey = (row, event) => {
const parent = row.parentNode;
const rows = DomHandler.find(parent, 'tr[data-p-selectable-row="true"]');

if (event.which === 9 && rows && rows.length > 0) {
const firstSelectedRow = DomHandler.findSingle(parent, 'tr[data-p-highlight="true"]');
const focusedItem = DomHandler.findSingle(parent, 'tr[data-p-selectable-row="true"][tabindex="0"]');

if (firstSelectedRow) {
firstSelectedRow.tabIndex = '0';
focusedItem && focusedItem !== firstSelectedRow && (focusedItem.tabIndex = '-1');
} else {
rows[0].tabIndex = '0';
focusedItem !== rows[0] && (rows[rowIndex].tabIndex = '-1');
}
}
};

const onMouseDown = (event) => {
props.onRowMouseDown({ originalEvent: event, data: props.rowData, index: props.rowIndex });
};
Expand Down Expand Up @@ -411,6 +492,7 @@ export const BodyRow = React.memo((props) => {
onDragLeave: (e) => onDragLeave(e),
onDragEnd: (e) => onDragEnd(e),
onDrop: (e) => onDrop(e),
'aria-selected': props?.selectionMode ? props.selected : null,
'data-p-selectable-row': props.allowRowSelection && props.isSelectable({ data: props.rowData, index: props.rowIndex }),
'data-p-highlight': props.selected,
'data-p-highlight-contextmenu': props.contextMenuSelected
Expand Down
27 changes: 19 additions & 8 deletions components/lib/datatable/ColumnFilter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,14 @@ import { InputText } from '../inputtext/InputText';
import { OverlayService } from '../overlayservice/OverlayService';
import { Portal } from '../portal/Portal';
import { Ripple } from '../ripple/Ripple';
import { DomHandler, IconUtils, ObjectUtils, ZIndexUtils } from '../utils/Utils';
import { DomHandler, IconUtils, ObjectUtils, UniqueComponentId, ZIndexUtils } from '../utils/Utils';
import { ariaLabel } from '../api/Locale';
import FocusTrap from '../focustrap/FocusTrap';

export const ColumnFilter = React.memo((props) => {
const [overlayVisibleState, setOverlayVisibleState] = React.useState(false);
const overlayRef = React.useRef(null);
const overlayId = React.useRef(UniqueComponentId());
const iconRef = React.useRef(null);
const selfClick = React.useRef(false);
const overlayEventListener = React.useRef(null);
Expand Down Expand Up @@ -540,16 +543,17 @@ export const ColumnFilter = React.memo((props) => {
const icon = props.filterIcon || <FilterIcon {...filterIconProps} />;
const columnFilterIcon = IconUtils.getJSXIcon(icon, { ...filterIconProps }, { props });

const label = filterLabel();
const label = overlayVisibleState ? ariaLabel('hideFilterMenu') : ariaLabel('showFilterMenu');
const filterMenuButtonProps = mergeProps(
{
type: 'button',
className: cx('filterMenuButton', { overlayVisibleState, hasFilter }),
'aria-haspopup': true,
'aria-expanded': overlayVisibleState,
'aria-label': label,
'aria-controls': overlayId.current,
onClick: (e) => toggleMenu(e),
onKeyDown: (e) => onToggleButtonKeyDown(e),
'aria-label': label
onKeyDown: (e) => onToggleButtonKeyDown(e)
},
getColumnPTOptions('filterMenuButton', {
context: {
Expand Down Expand Up @@ -685,6 +689,7 @@ export const ColumnFilter = React.memo((props) => {
pt={getColumnPTOptions('filterOperatorDropdown')}
unstyled={props.unstyled}
__parentMetadata={{ parent: props.metaData }}
aria-label={ariaLabel('filterOperator')}
/>
</div>
);
Expand All @@ -706,6 +711,7 @@ export const ColumnFilter = React.memo((props) => {
pt={getColumnPTOptions('filterMatchModeDropdown')}
unstyled={props.unstyled}
__parentMetadata={{ parent: props.metaData }}
aria-label={ariaLabel('filterConstraint')}
/>
);
}
Expand Down Expand Up @@ -872,7 +878,10 @@ export const ColumnFilter = React.memo((props) => {
className: cx('filterOverlay', { columnFilterProps: props, context, getColumnProp }),
onKeyDown: (e) => onContentKeyDown(e),
onClick: (e) => onContentClick(e),
onMouseDown: (e) => onContentMouseDown(e)
onMouseDown: (e) => onContentMouseDown(e),
id: overlayId.current,
'aria-modal': overlayVisibleState,
role: 'dialog'
},
getColumnPTOptions('filterOverlay')
);
Expand All @@ -895,9 +904,11 @@ export const ColumnFilter = React.memo((props) => {
<Portal>
<CSSTransition nodeRef={overlayRef} {...transitionProps}>
<div ref={overlayRef} {...filterOverlayProps}>
{filterHeader}
{items}
{filterFooter}
<FocusTrap autoFocus>
{filterHeader}
{items}
{filterFooter}
</FocusTrap>
</div>
</CSSTransition>
</Portal>
Expand Down
2 changes: 1 addition & 1 deletion components/lib/datatable/HeaderCell.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export const HeaderCell = React.memo((props) => {
};

const onKeyDown = (event) => {
if (event.key === 'Enter' && event.currentTarget === elementRef.current && DomHandler.getAttribute(event.currentTarget, 'data-p-sortable-column') === 'true') {
if ((event.which == 13 || event.which == 32) && event.currentTarget === elementRef.current && DomHandler.getAttribute(event.currentTarget, 'data-p-sortable-column') === 'true') {
onClick(event);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we want to use event.which anymore it should all be event.code right?


event.preventDefault();
Expand Down
2 changes: 2 additions & 0 deletions components/lib/datatable/HeaderCheckbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ColumnBase } from '../column/ColumnBase';
import { useMergeProps } from '../hooks/Hooks';
import { CheckIcon } from '../icons/check';
import { IconUtils } from '../utils/Utils';
import { ariaLabel } from '../api/Locale';

export const HeaderCheckbox = React.memo((props) => {
const [focusedState, setFocusedState] = React.useState(false);
Expand Down Expand Up @@ -78,6 +79,7 @@ export const HeaderCheckbox = React.memo((props) => {
className: cx('headerCheckbox', { headerProps: props, focusedState }),
role: 'checkbox',
'aria-checked': props.checked,
'aria-label': props.checked ? ariaLabel('selectAll') : ariaLabel('unselectAll'),
tabIndex: tabIndex,
onFocus: (e) => onFocus(e),
onBlur: (e) => onBlur(e),
Expand Down
3 changes: 2 additions & 1 deletion components/lib/datatable/TableBody.js
Original file line number Diff line number Diff line change
Expand Up @@ -1120,7 +1120,8 @@ export const TableBody = React.memo(
const tbodyProps = mergeProps(
{
style: props.style,
className: cx(ptKey, { className: props.className })
className: cx(ptKey, { className: props.className }),
role: ' rowgroup'
},
ptm(ptKey, { hostName: props.hostName })
);
Expand Down
3 changes: 2 additions & 1 deletion components/lib/datatable/TableFooter.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export const TableFooter = React.memo((props) => {
const content = createContent();
const tfootProps = mergeProps(
{
className: cx('tfoot')
className: cx('tfoot'),
role: 'rowgroup'
},
getColumnGroupPTOptions('root'),
ptm('tfoot', { hostName: props.hostName })
Expand Down
3 changes: 2 additions & 1 deletion components/lib/datatable/TableHeader.js
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,8 @@ export const TableHeader = React.memo((props) => {
const content = createContent();
const theadProps = mergeProps(
{
className: cx('thead')
className: cx('thead'),
role: 'rowgroup'
},
getColumnGroupPTOptions('root'),
ptm('thead', { hostName: props.hostName })
Expand Down
Loading