Skip to content
This repository was archived by the owner on Aug 8, 2024. It is now read-only.

fix: table moves up when doing selection #559

Closed
wants to merge 13 commits into from
195 changes: 101 additions & 94 deletions src/table/components/TableBodyWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, memo } from 'react';
import React, { useEffect, useMemo, memo, forwardRef } from 'react';
import PropTypes from 'prop-types';
import getCellRenderer from '../utils/get-cell-renderer';
import { useContextSelector, TableContext } from '../context';
Expand All @@ -8,102 +8,109 @@ import { getBodyCellStyle } from '../utils/styling-utils';
import { bodyHandleKeyPress, bodyHandleKeyUp } from '../utils/handle-key-press';
import { handleClickToFocusBody } from '../utils/handle-accessibility';

function TableBodyWrapper({
rootElement,
tableData,
constraints,
selectionsAPI,
layout,
theme,
setShouldRefocus,
keyboard,
tableWrapperRef,
announce,
children,
}) {
const { rows, columns, paginationNeeded, totalsPosition } = tableData;
const columnsStylingInfoJSON = JSON.stringify(columns.map((column) => column.stylingInfo));
const setFocusedCellCoord = useContextSelector(TableContext, (value) => value.setFocusedCellCoord);
const selectionDispatch = useContextSelector(TableContext, (value) => value.selectionDispatch);
// constraints.active: true - turn off interactions that affect the state of the visual
// representation including selection, zoom, scroll, etc.
// constraints.select: true - turn off selections.
const isSelectionsEnabled = !constraints.active && !constraints.select;
const columnRenderers = useMemo(
() =>
JSON.parse(columnsStylingInfoJSON).map((stylingInfo) =>
getCellRenderer(!!stylingInfo.length, isSelectionsEnabled)
),
[columnsStylingInfoJSON, isSelectionsEnabled]
);
const bodyCellStyle = useMemo(() => getBodyCellStyle(layout, theme), [layout, theme]);
const hoverEffect = layout.components?.[0]?.content?.hoverEffect;
const cellStyle = { color: bodyCellStyle.color, backgroundColor: theme.table.backgroundColor };
const TableBodyWrapper = forwardRef(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

(
{
rootElement,
tableData,
constraints,
selectionsAPI,
layout,
theme,
setShouldRefocus,
keyboard,
tableWrapperRef,
announce,
children,
},
ref
) => {
const { rows, columns, paginationNeeded, totalsPosition } = tableData;
const columnsStylingInfoJSON = JSON.stringify(columns.map((column) => column.stylingInfo));
const setFocusedCellCoord = useContextSelector(TableContext, (value) => value.setFocusedCellCoord);
const selectionDispatch = useContextSelector(TableContext, (value) => value.selectionDispatch);
// constraints.active: true - turn off interactions that affect the state of the visual
// representation including selection, zoom, scroll, etc.
// constraints.select: true - turn off selections.
const isSelectionsEnabled = !constraints.active && !constraints.select;
const columnRenderers = useMemo(
() =>
JSON.parse(columnsStylingInfoJSON).map((stylingInfo) =>
getCellRenderer(!!stylingInfo.length, isSelectionsEnabled)
),
[columnsStylingInfoJSON, isSelectionsEnabled]
);
const bodyCellStyle = useMemo(() => getBodyCellStyle(layout, theme), [layout, theme]);
const hoverEffect = layout.components?.[0]?.content?.hoverEffect;
const cellStyle = { color: bodyCellStyle.color, backgroundColor: theme.table.backgroundColor };

useEffect(() => {
addSelectionListeners({ api: selectionsAPI, selectionDispatch, setShouldRefocus, keyboard, tableWrapperRef });
}, []);
useEffect(() => {
addSelectionListeners({ api: selectionsAPI, selectionDispatch, setShouldRefocus, keyboard, tableWrapperRef });
}, []);

return (
<StyledTableBody paginationNeeded={paginationNeeded} bodyCellStyle={bodyCellStyle}>
{totalsPosition === 'top' ? children : undefined}
{rows.map((row) => (
<StyledBodyRow
bodyCellStyle={bodyCellStyle}
hover={hoverEffect}
tabIndex={-1}
key={row.key}
className="sn-table-row"
>
{columns.map((column, columnIndex) => {
const { id, align } = column;
const cell = row[id];
const CellRenderer = columnRenderers[columnIndex];
const handleKeyDown = (evt) => {
bodyHandleKeyPress({
evt,
rootElement,
selectionsAPI,
cell,
selectionDispatch,
isSelectionsEnabled,
setFocusedCellCoord,
announce,
keyboard,
paginationNeeded,
totalsPosition,
});
};
return (
<StyledTableBody ref={ref} paginationNeeded={paginationNeeded} bodyCellStyle={bodyCellStyle}>
{totalsPosition === 'top' ? children : undefined}
{rows.map((row) => (
<StyledBodyRow
bodyCellStyle={bodyCellStyle}
hover={hoverEffect}
tabIndex={-1}
key={row.key}
className="sn-table-row"
>
{columns.map((column, columnIndex) => {
const { id, align } = column;
const cell = row[id];
const CellRenderer = columnRenderers[columnIndex];
const handleKeyDown = (evt) => {
bodyHandleKeyPress({
evt,
rootElement,
selectionsAPI,
cell,
selectionDispatch,
isSelectionsEnabled,
setFocusedCellCoord,
announce,
keyboard,
paginationNeeded,
totalsPosition,
});
};

return (
CellRenderer && (
<CellRenderer
scope={columnIndex === 0 ? 'row' : null}
component={columnIndex === 0 ? 'th' : null}
cell={cell}
column={column}
key={id}
align={align}
styling={cellStyle}
tabIndex={-1}
announce={announce}
onKeyDown={handleKeyDown}
onKeyUp={(evt) => bodyHandleKeyUp(evt, selectionDispatch)}
onMouseDown={() =>
handleClickToFocusBody(cell, rootElement, setFocusedCellCoord, keyboard, totalsPosition)
}
>
{cell.qText}
</CellRenderer>
)
);
})}
</StyledBodyRow>
))}
{totalsPosition === 'bottom' ? children : undefined}
</StyledTableBody>
);
}
return (
CellRenderer && (
<CellRenderer
scope={columnIndex === 0 ? 'row' : null}
component={columnIndex === 0 ? 'th' : null}
cell={cell}
column={column}
key={id}
align={align}
styling={cellStyle}
tabIndex={-1}
announce={announce}
onKeyDown={handleKeyDown}
onKeyUp={(evt) => bodyHandleKeyUp(evt, selectionDispatch)}
onMouseDown={() =>
handleClickToFocusBody(cell, rootElement, setFocusedCellCoord, keyboard, totalsPosition)
}
>
{cell.qText}
</CellRenderer>
)
);
})}
</StyledBodyRow>
))}
{totalsPosition === 'bottom' ? children : undefined}
</StyledTableBody>
);
}
);

TableBodyWrapper.displayName = 'TableBodyWrapper';

TableBodyWrapper.propTypes = {
rootElement: PropTypes.object.isRequired,
Expand Down
2 changes: 1 addition & 1 deletion src/table/components/TableTotals.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ function TableTotals({ rootElement, tableData, theme, layout, keyboard }) {
const isTop = totalsPosition === 'top';

return (
<StyledHeadRow paginationNeeded={paginationNeeded} className="sn-table-row">
<StyledHeadRow paginationNeeded={paginationNeeded} className="sn-table-totals sn-table-row">
{columns.map((column, columnIndex) => {
const cellCoord = [isTop ? 1 : rows.length + 1, columnIndex];
return (
Expand Down
23 changes: 13 additions & 10 deletions src/table/components/TableWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
import React, { useEffect, useRef, useCallback } from 'react';
import React, { useRef, useCallback } from 'react';
import Table from '@mui/material/Table';

import AnnounceElements from './AnnounceElements';
Expand All @@ -9,14 +9,13 @@ import TableTotals from './TableTotals';
import FooterWrapper from './FooterWrapper';
import { useContextSelector, TableContext } from '../context';
import { StyledTableContainer, StyledTableWrapper } from '../styles';

import PaginationContent from './PaginationContent';
import useDidUpdateEffect from '../hooks/use-did-update-effect';
import useFocusListener from '../hooks/use-focus-listener';
import useScrollListener from '../hooks/use-scroll-listener';
import useKeyDownListener from '../hooks/use-key-down-listener';
import { handleTableWrapperKeyDown } from '../utils/handle-key-press';
import { updateFocus, handleResetFocus, getCellElement } from '../utils/handle-accessibility';
import { handleNavigateTop } from '../utils/handle-scroll';

export default function TableWrapper(props) {
const {
Expand All @@ -33,14 +32,15 @@ export default function TableWrapper(props) {
footerContainer,
announce,
} = props;
const { totalColumnCount, totalRowCount, totalPages, paginationNeeded, rows, columns } = tableData;
const { totalColumnCount, totalRowCount, totalPages, paginationNeeded, rows, columns, totalsPosition } = tableData;
const { page, rowsPerPage } = pageInfo;
const isSelectionMode = selectionsAPI.isModal();
const focusedCellCoord = useContextSelector(TableContext, (value) => value.focusedCellCoord);
const setFocusedCellCoord = useContextSelector(TableContext, (value) => value.setFocusedCellCoord);
const shouldRefocus = useRef(false);
const tableContainerRef = useRef();
const tableWrapperRef = useRef();
const tableBodyWrapperRef = useRef();

const setShouldRefocus = useCallback(() => {
shouldRefocus.current = rootElement.getElementsByTagName('table')[0].contains(document.activeElement);
Expand Down Expand Up @@ -72,11 +72,7 @@ export default function TableWrapper(props) {

useFocusListener(tableWrapperRef, shouldRefocus, keyboard);
useScrollListener(tableContainerRef, direction);

useEffect(
() => handleNavigateTop({ tableContainerRef, focusedCellCoord, rootElement }),
[tableContainerRef, focusedCellCoord, rootElement]
);
useKeyDownListener(tableBodyWrapperRef, focusedCellCoord, rootElement, totalsPosition);

useDidUpdateEffect(() => {
// When nebula handles keyboard navigation and keyboard.active changes,
Expand Down Expand Up @@ -112,9 +108,11 @@ export default function TableWrapper(props) {
paginationNeeded={paginationNeeded}
dir={direction}
onKeyDown={handleKeyDown}
data-testid="table-wrapper"
>
<AnnounceElements />
<StyledTableContainer
className="sn-table-container"
ref={tableContainerRef}
fullHeight={footerContainer || constraints.active || !paginationNeeded} // the footerContainer always wants height: 100%
constraints={constraints}
Expand All @@ -124,7 +122,12 @@ export default function TableWrapper(props) {
>
<Table stickyHeader aria-label={tableAriaLabel}>
<TableHeadWrapper {...props} />
<TableBodyWrapper {...props} setShouldRefocus={setShouldRefocus} tableWrapperRef={tableWrapperRef}>
<TableBodyWrapper
{...props}
ref={tableBodyWrapperRef}
setShouldRefocus={setShouldRefocus}
tableWrapperRef={tableWrapperRef}
>
<TableTotals {...props} />
</TableBodyWrapper>
</Table>
Expand Down
18 changes: 17 additions & 1 deletion src/table/components/__tests__/TableWrapper.spec.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import TableWrapper from '../TableWrapper';
import TableBodyWrapper from '../TableBodyWrapper';
import TableHeadWrapper from '../TableHeadWrapper';
import * as handleKeyPress from '../../utils/handle-key-press';
import * as handleAccessibility from '../../utils/handle-accessibility';
import * as handleScroll from '../../utils/handle-scroll';

describe('<TableWrapper />', () => {
Expand Down Expand Up @@ -42,7 +43,13 @@ describe('<TableWrapper />', () => {
beforeEach(() => {
// When wrapping a component in memo, the actual functional component is stored on type
jest.spyOn(TableHeadWrapper, 'type').mockImplementation(() => <thead />);
jest.spyOn(TableBodyWrapper, 'type').mockImplementation(() => <thead />);
// When wrapping a component in forwardRef, the actual functional component is stored on type.render
jest.spyOn(TableBodyWrapper.type, 'render').mockImplementation(() => (
<tbody>
<td>test</td>
</tbody>
));
jest.spyOn(handleKeyPress, 'handleTableWrapperKeyDown').mockImplementation(() => jest.fn());

tableData = {
totalRowCount: 200,
Expand All @@ -69,6 +76,7 @@ describe('<TableWrapper />', () => {
width: 750,
};
theme = {
name: () => {},
getStyle: () => {},
table: {
body: {
Expand Down Expand Up @@ -115,6 +123,14 @@ describe('<TableWrapper />', () => {
expect(handleKeyPress.handleTableWrapperKeyDown).toHaveBeenCalledTimes(1);
});

it('should call handleFocusoutEvent when table is focus out', () => {
jest.spyOn(handleAccessibility, 'handleFocusoutEvent').mockImplementation(() => jest.fn());
const { queryByTestId } = renderTableWrapper();

fireEvent.focusOut(queryByTestId('table-wrapper'));
expect(handleAccessibility.handleFocusoutEvent).toHaveBeenCalledTimes(1);
});

it('should call handleHorizontalScroll when scroll on the table', () => {
jest.spyOn(handleScroll, 'handleHorizontalScroll').mockImplementation(() => jest.fn());
const { queryByTestId } = renderTableWrapper();
Expand Down
19 changes: 19 additions & 0 deletions src/table/hooks/use-key-down-listener.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { useEffect } from 'react';
import { handleNavigateTop } from '../utils/handle-scroll';

const useKeyDownListener = (tableBodyWrapperRef, focusedCellCoord, rootElement, totalsPosition) => {
useEffect(() => {
const memoedContainer = tableBodyWrapperRef.current;
if (!memoedContainer) return undefined;

const keyDownHandler = (evt) =>
evt.key === 'ArrowUp' && handleNavigateTop({ focusedCellCoord, rootElement, totalsPosition });
memoedContainer.addEventListener('keyup', keyDownHandler);

return () => {
memoedContainer.removeEventListener('keyup', keyDownHandler);
};
}, [tableBodyWrapperRef, focusedCellCoord, rootElement, totalsPosition]);
};

export default useKeyDownListener;
1 change: 0 additions & 1 deletion src/table/utils/__tests__/handle-key-press.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
handleLastTab,
totalHandleKeyPress,
} from '../handle-key-press';

import * as handleAccessibility from '../handle-accessibility';

describe('handle-key-press', () => {
Expand Down
Loading