diff --git a/components/lib/datatable/BodyCell.js b/components/lib/datatable/BodyCell.js index f21b6a5092..5e48628200 100644 --- a/components/lib/datatable/BodyCell.js +++ b/components/lib/datatable/BodyCell.js @@ -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 {content}; }; @@ -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') @@ -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') ); @@ -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') diff --git a/components/lib/datatable/BodyRow.js b/components/lib/datatable/BodyRow.js index 207164643b..a5ce074112 100644 --- a/components/lib/datatable/BodyRow.js +++ b/components/lib/datatable/BodyRow.js @@ -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; @@ -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; @@ -149,47 +161,33 @@ export const BodyRow = React.memo((props) => { if (isFocusable() && !props.allowCellSelection) { const { target, currentTarget: row } = event; - switch (event.which) { - //down arrow - case 40: - let nextRow = findNextSelectableRow(row); - - if (nextRow) { - changeTabIndex(row, nextRow); - nextRow.focus(); - } - - event.preventDefault(); + switch (event.code) { + case 'ArrowDown': + onArrowDownKey(row, event); break; - //up arrow - case 38: - let prevRow = findPrevSelectableRow(row); - - if (prevRow) { - changeTabIndex(row, prevRow); - prevRow.focus(); - } + case 'ArrowUp': + onArrowUpKey(row, event); + break; - event.preventDefault(); + case 'Home': + onHomeKey(row, event); break; - //enter - case 13: // @deprecated - if (!DomHandler.isClickable(target)) { - onClick(event); - event.preventDefault(); - } + case 'End': + onEndKey(row, event); + break; + case 'Enter': + onEnterKey(row, event, target); break; - //space - case 32: - if (!DomHandler.isClickable(target) && !target.readOnly) { - onClick(event); - event.preventDefault(); - } + case 'Space': + onSpaceKey(row, event, target); + break; + case 'Tab': + onTabKey(row, event); break; default: @@ -199,6 +197,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.code === 'Tab' && 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 }); }; @@ -411,6 +485,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 diff --git a/components/lib/datatable/ColumnFilter.js b/components/lib/datatable/ColumnFilter.js index 46397be706..b92c68c472 100644 --- a/components/lib/datatable/ColumnFilter.js +++ b/components/lib/datatable/ColumnFilter.js @@ -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); @@ -540,16 +543,17 @@ export const ColumnFilter = React.memo((props) => { const icon = props.filterIcon || ; 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: { @@ -685,6 +689,7 @@ export const ColumnFilter = React.memo((props) => { pt={getColumnPTOptions('filterOperatorDropdown')} unstyled={props.unstyled} __parentMetadata={{ parent: props.metaData }} + aria-label={ariaLabel('filterOperator')} /> ); @@ -706,6 +711,7 @@ export const ColumnFilter = React.memo((props) => { pt={getColumnPTOptions('filterMatchModeDropdown')} unstyled={props.unstyled} __parentMetadata={{ parent: props.metaData }} + aria-label={ariaLabel('filterConstraint')} /> ); } @@ -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') ); @@ -895,9 +904,11 @@ export const ColumnFilter = React.memo((props) => {
- {filterHeader} - {items} - {filterFooter} + + {filterHeader} + {items} + {filterFooter} +
diff --git a/components/lib/datatable/HeaderCell.js b/components/lib/datatable/HeaderCell.js index 84bd622cc9..5de529237b 100644 --- a/components/lib/datatable/HeaderCell.js +++ b/components/lib/datatable/HeaderCell.js @@ -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.code == 'Enter' || event.code == 'Space') && event.currentTarget === elementRef.current && DomHandler.getAttribute(event.currentTarget, 'data-p-sortable-column') === 'true') { onClick(event); event.preventDefault(); diff --git a/components/lib/datatable/HeaderCheckbox.js b/components/lib/datatable/HeaderCheckbox.js index 4c8d37019e..9e5eff621d 100644 --- a/components/lib/datatable/HeaderCheckbox.js +++ b/components/lib/datatable/HeaderCheckbox.js @@ -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); @@ -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), diff --git a/components/lib/datatable/TableBody.js b/components/lib/datatable/TableBody.js index 17ec880027..c28e717906 100644 --- a/components/lib/datatable/TableBody.js +++ b/components/lib/datatable/TableBody.js @@ -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 }) ); diff --git a/components/lib/datatable/TableFooter.js b/components/lib/datatable/TableFooter.js index a5602eca80..6409f3a33b 100644 --- a/components/lib/datatable/TableFooter.js +++ b/components/lib/datatable/TableFooter.js @@ -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 }) diff --git a/components/lib/datatable/TableHeader.js b/components/lib/datatable/TableHeader.js index 570f797b0b..681c3db56e 100644 --- a/components/lib/datatable/TableHeader.js +++ b/components/lib/datatable/TableHeader.js @@ -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 })