UNPKG

@ui5/webcomponents-react

Version:

React Wrapper for UI5 Web Components and additional components

370 lines (369 loc) 15 kB
import { useCallback, useEffect, useRef } from 'react'; import { actions } from '../react-table/index.js'; import { getLeafHeaders, NAVIGATION_KEYS } from '../util/index.js'; const CELL_DATA_ATTRIBUTES = ['visibleColumnIndex', 'columnIndex', 'rowIndex', 'visibleRowIndex']; const getFirstVisibleCell = (target, currentlyFocusedCell, noData) => { if (target.dataset.componentName === 'AnalyticalTableContainer' && target.querySelector('[data-component-name="AnalyticalTableBodyScrollableContainer"]')) { const rowElements = target.querySelector('[data-component-name="AnalyticalTableBodyScrollableContainer"]').children; const middleRowCell = target.querySelector(`div[data-visible-column-index="0"][data-visible-row-index="${Math.round(rowElements.length / 2)}"]`); middleRowCell?.focus({ preventScroll: true }); } else { const firstVisibleCell = noData ? target.querySelector(`div[data-visible-column-index="0"][data-visible-row-index="0"]`) : target.querySelector(`div[data-visible-column-index="0"][data-visible-row-index="1"]`); if (firstVisibleCell) { firstVisibleCell.tabIndex = 0; firstVisibleCell.focus(); currentlyFocusedCell.current = firstVisibleCell; } } }; function recursiveSubComponentElementSearch(element) { if (!element.parentElement) { return null; } if (element?.parentElement.dataset.subcomponent) { return element.parentElement; } return recursiveSubComponentElementSearch(element.parentElement); } const findParentCell = target => { if (target === undefined || target === null) return; if (target.dataset.rowIndex !== undefined && target.dataset.columnIndex !== undefined || target.dataset.rowIndexSub !== undefined && target.dataset.columnIndexSub !== undefined) { return target; } else { return findParentCell(target.parentElement); } }; const setFocus = (currentlyFocusedCell, nextElement) => { currentlyFocusedCell.current.tabIndex = -1; if (nextElement) { nextElement.tabIndex = 0; nextElement.focus(); currentlyFocusedCell.current = nextElement; } }; const navigateFromActiveSubCompItem = (currentlyFocusedCell, e) => { setFocus(currentlyFocusedCell, recursiveSubComponentElementSearch(e.target)); }; const scrollToHorizontalEdgeAndFocus = (tableRef, scrollLeft, targetSelector, currentlyFocusedCell) => { tableRef.current.scrollLeft = scrollLeft; requestAnimationFrame(() => { const el = tableRef.current.querySelector(targetSelector); if (el) { setFocus(currentlyFocusedCell, el); } }); }; const useGetTableProps = (tableProps, { instance: { webComponentsReactProperties, data, columns, state, visibleColumns } }) => { const { showOverlay, tableRef } = webComponentsReactProperties; const { isRtl } = state; const currentlyFocusedCell = useRef(null); const noData = data.length === 0; useEffect(() => { if (showOverlay && currentlyFocusedCell.current) { currentlyFocusedCell.current.tabIndex = -1; currentlyFocusedCell.current = null; } }, [showOverlay]); const onTableBlur = e => { if (e.target.tagName === 'UI5-LI' || e.target.tagName === 'UI5-LI-CUSTOM') { currentlyFocusedCell.current = null; } }; useEffect(() => { if (!showOverlay && data && columns && currentlyFocusedCell.current && tableRef.current && tableRef.current.tabIndex !== 0 && !tableRef.current.contains(currentlyFocusedCell.current)) { currentlyFocusedCell.current = null; tableRef.current.tabIndex = 0; } }, [data, columns, showOverlay, tableRef]); const onTableFocus = useCallback(e => { const { dataset } = e.target; if (dataset.emptyRowCell === 'true' || Object.prototype.hasOwnProperty.call(dataset, 'subcomponentActiveElement') || dataset.componentName === 'ATHeaderPopoverList' || dataset.componentName === 'ATHeaderPopover' || dataset.componentName === 'AnalyticalTableNoDataContainer') { return; } if (e.target.dataset.subcomponent) { e.target.tabIndex = 0; e.target.focus(); currentlyFocusedCell.current = e.target; return; } const isFirstCellAvailable = e.target.querySelector('div[data-column-index="0"][data-row-index="1"]'); if (e.target.dataset.componentName === 'AnalyticalTableContainer') { e.target.tabIndex = -1; if (currentlyFocusedCell.current) { const { dataset } = currentlyFocusedCell.current; const rowIndex = parseInt(dataset.rowIndex ?? dataset.rowIndexSub, 10); const columnIndex = parseInt(dataset.columnIndex ?? dataset.columnIndexSub, 10); if (e.target.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex}"]`) || e.target.querySelector(`div[data-column-index-sub="${columnIndex}"][data-row-index-sub="${rowIndex}"]`)) { currentlyFocusedCell.current.tabIndex = 0; currentlyFocusedCell.current.focus({ preventScroll: true }); } else { getFirstVisibleCell(e.target, currentlyFocusedCell, noData); } } else if (isFirstCellAvailable) { const firstCell = e.target.querySelector('div[data-column-index]:not([data-column-id^="__ui5wcr__internal"][data-row-index="0"])'); firstCell.tabIndex = 0; firstCell.focus({ preventScroll: true }); currentlyFocusedCell.current = firstCell; } else { getFirstVisibleCell(e.target, currentlyFocusedCell, noData); } } else { const tableCell = findParentCell(e.target); if (tableCell) { currentlyFocusedCell.current = tableCell; } else { getFirstVisibleCell(tableRef.current, currentlyFocusedCell, noData); } } }, [noData, tableRef]); const onKeyboardNavigation = useCallback(e => { const isActiveItemInSubComponent = Object.prototype.hasOwnProperty.call(e.target.dataset, 'subcomponentActiveElement'); // check if target is cell and if so proceed from there if (!currentlyFocusedCell.current && CELL_DATA_ATTRIBUTES.every(item => Object.keys(e.target.dataset).includes(item))) { currentlyFocusedCell.current = e.target; } if (currentlyFocusedCell.current) { const columnIndex = parseInt(currentlyFocusedCell.current.dataset.columnIndex ?? '0', 10); const rowIndex = parseInt(currentlyFocusedCell.current.dataset.rowIndex ?? currentlyFocusedCell.current.dataset.subcomponentRowIndex, 10); if (NAVIGATION_KEYS.has(e.key)) { e.preventDefault(); } switch (e.key) { case 'End': { const lastColumnIndex = visibleColumns.length - 1; const targetSelector = `div[data-column-index="${lastColumnIndex}"][data-row-index="${rowIndex}"]`; const newElement = tableRef.current.querySelector(targetSelector); if (newElement) { setFocus(currentlyFocusedCell, newElement); } else { scrollToHorizontalEdgeAndFocus(tableRef, isRtl ? 0 : tableRef.current.scrollWidth, targetSelector, currentlyFocusedCell); } break; } case 'Home': { const targetSelector = `div[data-column-index="0"][data-row-index="${rowIndex}"]`; const newElement = tableRef.current.querySelector(targetSelector); if (newElement) { setFocus(currentlyFocusedCell, newElement); } else { scrollToHorizontalEdgeAndFocus(tableRef, isRtl ? tableRef.current.scrollWidth : 0, targetSelector, currentlyFocusedCell); } break; } case 'PageDown': { if (currentlyFocusedCell.current.dataset.rowIndex === '0') { const newElement = tableRef.current.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`); setFocus(currentlyFocusedCell, newElement); } else { const lastVisibleRow = tableRef.current.querySelector(`div[data-component-name="AnalyticalTableBody"]`)?.children?.[0].children.length; const newElement = tableRef.current.querySelector(`div[data-column-index="${columnIndex}"][data-visible-row-index="${lastVisibleRow}"]`); setFocus(currentlyFocusedCell, newElement); } break; } case 'PageUp': { if (currentlyFocusedCell.current.dataset.rowIndex <= '1') { const newElement = tableRef.current.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="0"]`); setFocus(currentlyFocusedCell, newElement); } else { const newElement = tableRef.current.querySelector(`div[data-column-index="${columnIndex}"][data-visible-row-index="1"]`); setFocus(currentlyFocusedCell, newElement); } break; } case 'ArrowRight': { if (isActiveItemInSubComponent) { navigateFromActiveSubCompItem(currentlyFocusedCell, e); return; } const newElement = tableRef.current.querySelector(`div[data-column-index="${columnIndex + (isRtl ? -1 : 1)}"][data-row-index="${rowIndex}"]`); if (newElement) { setFocus(currentlyFocusedCell, newElement); // scroll to show full cell if it's only partial visible newElement.scrollIntoView({ block: 'nearest' }); } break; } case 'ArrowLeft': { if (isActiveItemInSubComponent) { navigateFromActiveSubCompItem(currentlyFocusedCell, e); return; } const newElement = tableRef.current.querySelector(`div[data-column-index="${columnIndex - (isRtl ? -1 : 1)}"][data-row-index="${rowIndex}"]`); if (newElement) { setFocus(currentlyFocusedCell, newElement); // scroll to show full cell if it's only partial visible newElement.scrollIntoView({ block: 'nearest' }); } break; } case 'ArrowDown': { if (isActiveItemInSubComponent) { navigateFromActiveSubCompItem(currentlyFocusedCell, e); return; } const parent = currentlyFocusedCell.current.parentElement; const firstChildOfParent = parent?.children?.[0]; const hasSubcomponent = firstChildOfParent?.dataset?.subcomponent; const newElement = tableRef.current.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${rowIndex + 1}"]`); if (hasSubcomponent && !currentlyFocusedCell.current?.dataset?.subcomponent) { currentlyFocusedCell.current.tabIndex = -1; // Happens outside of React's scope // eslint-disable-next-line react-hooks/immutability firstChildOfParent.tabIndex = 0; firstChildOfParent.dataset.rowIndexSub = `${rowIndex}`; firstChildOfParent.dataset.columnIndexSub = `${columnIndex}`; firstChildOfParent.focus(); currentlyFocusedCell.current = firstChildOfParent; } else if (newElement) { setFocus(currentlyFocusedCell, newElement); } break; } case 'ArrowUp': { if (isActiveItemInSubComponent) { navigateFromActiveSubCompItem(currentlyFocusedCell, e); return; } let prevRowIndex = rowIndex - 1; const isSubComponent = e.target.dataset.subcomponent; if (isSubComponent) { prevRowIndex++; } const previousRowCell = tableRef.current.querySelector(`div[data-column-index="${columnIndex}"][data-row-index="${prevRowIndex}"]`); const firstChildPrevRow = previousRowCell?.parentElement.children[0]; const hasSubcomponent = firstChildPrevRow?.dataset?.subcomponent; if (hasSubcomponent && !isSubComponent) { currentlyFocusedCell.current.tabIndex = -1; firstChildPrevRow.dataset.rowIndexSub = `${rowIndex - 1}`; firstChildPrevRow.dataset.columnIndexSub = `${columnIndex}`; firstChildPrevRow.tabIndex = 0; firstChildPrevRow.focus(); currentlyFocusedCell.current = firstChildPrevRow; } else if (previousRowCell) { setFocus(currentlyFocusedCell, previousRowCell); } break; } } } }, [isRtl, tableRef, visibleColumns]); if (showOverlay) { return tableProps; } // keyboard nav is only enabled if the table is not in edit mode const handleEditModeKeyDown = e => { if (typeof tableProps.onKeyDown === 'function') { tableProps.onKeyDown(e); } }; return [tableProps, { onFocus: onTableFocus, onKeyDown: state.cellContentTabIndex === 0 ? handleEditModeKeyDown : onKeyboardNavigation, onBlur: onTableBlur }]; }; function getPayload(e, column) { e.preventDefault(); e.stopPropagation(); const target = e.target; const clientX = target.getBoundingClientRect().x + target.getBoundingClientRect().width; const columnId = column.id; const columnWidth = column.totalWidth; const headersToResize = getLeafHeaders(column); const headerIdWidths = headersToResize.map(d => [d.id, d.totalWidth, d.minWidth, d.maxWidth]); return { clientX, columnId, columnWidth, headerIdWidths }; } const setHeaderProps = (headerProps, { instance: { dispatch }, column }) => { // resize col with keyboard const handleKeyDown = e => { if (typeof headerProps.onKeyDown === 'function') { headerProps.onKeyDown(e); } if (e.nativeEvent.shiftKey) { if (e.key === 'ArrowRight') { const payload = getPayload(e, column); dispatch({ type: actions.columnStartResizing, ...payload }); dispatch({ type: actions.columnResizing, clientX: payload.clientX + 16 }); dispatch({ type: actions.columnDoneResizing }); return; } if (e.key === 'ArrowLeft') { const payload = getPayload(e, column); dispatch({ type: actions.columnStartResizing, ...payload }); dispatch({ type: actions.columnResizing, clientX: payload.clientX - 16 }); dispatch({ type: actions.columnDoneResizing }); return; } } }; return [headerProps, { onKeyDown: handleKeyDown }]; }; export const useKeyboardNavigation = hooks => { hooks.getTableProps.push(useGetTableProps); hooks.getHeaderProps.push(setHeaderProps); }; useKeyboardNavigation.pluginName = 'useKeyboardNavigation';