Skip to content

Commit 3b390fd

Browse files
jluyauLFDanLusnowystingerdannify
authored
Keep focus on last pressed column header through loading state (#4249)
* Keep focus on last pressed column header through loading state * fix lint * add comment * add manager focused condition * no tabbing to column headers when table is empty * cleanup focus if table becomes empty * fix lint * fix focus target on blur * update story to trigger empty state on 2nd press of table column header * simplify * fix tests * fix issue where items cutoff focus ring on entire collection * Add test * revert logic to focus first item * fix test * fix box shadow cutoff --------- Co-authored-by: Daniel Lu <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Danni <[email protected]>
1 parent 9669c44 commit 3b390fd

File tree

4 files changed

+89
-14
lines changed

4 files changed

+89
-14
lines changed

packages/@adobe/spectrum-css-temp/components/table/skin.css

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,15 @@ governing permissions and limitations under the License.
103103
background-color: var(--spectrum-table-background-color);
104104

105105
&:focus-ring {
106-
box-shadow: inset 0 0 0 2px var(--spectrum-table-cell-border-color-key-focus);
106+
&::after {
107+
content: "";
108+
height: 100%;
109+
position: absolute;
110+
width: 100%;
111+
left: 0px;
112+
top: 0px;
113+
box-shadow: inset 0 0 0 2px var(--spectrum-table-cell-border-color-key-focus);
114+
}
107115
}
108116

109117
&.is-drop-target {

packages/@react-aria/table/src/useTableColumnHeader.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import {GridNode} from '@react-types/grid';
1616
// @ts-ignore
1717
import intlMessages from '../intl/*.json';
1818
import {isAndroid, mergeProps, useDescription} from '@react-aria/utils';
19-
import {RefObject} from 'react';
19+
import {RefObject, useEffect} from 'react';
2020
import {TableState} from '@react-stately/table';
2121
import {useFocusable} from '@react-aria/focus';
2222
import {useGridCell} from '@react-aria/grid';
@@ -80,15 +80,22 @@ export function useTableColumnHeader<T>(props: AriaTableColumnHeaderProps<T>, st
8080

8181
let descriptionProps = useDescription(sortDescription);
8282

83+
let shouldDisableFocus = state.collection.size === 0;
84+
useEffect(() => {
85+
if (shouldDisableFocus && state.selectionManager.focusedKey === node.key) {
86+
state.selectionManager.setFocusedKey(null);
87+
}
88+
}, [shouldDisableFocus, state.selectionManager, node.key]);
89+
8390
return {
8491
columnHeaderProps: {
8592
...mergeProps(
8693
gridCellProps,
8794
pressProps,
8895
focusableProps,
8996
descriptionProps,
90-
// If the table is empty, make all column headers untabbable or programatically focusable
91-
state.collection.size === 0 && {tabIndex: null}
97+
// If the table is empty, make all column headers untabbable
98+
shouldDisableFocus && {tabIndex: -1}
9299
),
93100
role: 'columnheader',
94101
id: getColumnHeaderId(state, node.key),

packages/@react-spectrum/table/stories/Table.stories.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1805,6 +1805,7 @@ let typeAheadRows = [
18051805
...Array.from({length: 100}, (v, i) => ({id: i, firstname: 'Aubrey', lastname: 'Sheppard', birthday: 'May 7'})),
18061806
{id: 101, firstname: 'John', lastname: 'Doe', birthday: 'May 7'}
18071807
];
1808+
18081809
export const TypeaheadWithDialog: TableStory = {
18091810
render: (args) => (
18101811
<div style={{height: '90vh'}}>
@@ -1844,3 +1845,48 @@ export const TypeaheadWithDialog: TableStory = {
18441845
</div>
18451846
)
18461847
};
1848+
1849+
export const ColumnHeaderFocusRingTable = {
1850+
render: () => <LoadingTable />,
1851+
storyName: 'column header focus after loading',
1852+
parameters: {
1853+
description: {
1854+
data: 'Column header should remain focused even if the table collections empties/loading state changes to loading'
1855+
}
1856+
}
1857+
};
1858+
1859+
const allItems = [
1860+
{key: 'sam', name: 'Sam', height: 66, birthday: 'May 3'},
1861+
{key: 'julia', name: 'Julia', height: 70, birthday: 'February 10'}
1862+
];
1863+
1864+
function LoadingTable() {
1865+
const [items, setItems] = useState(allItems);
1866+
const [loadingState, setLoadingState] = useState(undefined);
1867+
const onSortChange = () => {
1868+
setItems([]);
1869+
setLoadingState('loading');
1870+
setTimeout(() => {
1871+
setItems(items.length > 1 ? [...items.slice(0, 1)] : []);
1872+
setLoadingState(undefined);
1873+
}, 1000);
1874+
};
1875+
1876+
return (
1877+
<TableView aria-label="Table" selectionMode="multiple" onSortChange={onSortChange} height={300}>
1878+
<TableHeader>
1879+
<Column key="name">Name</Column>
1880+
<Column key="height" allowsSorting>Height</Column>
1881+
<Column key="birthday">Birthday</Column>
1882+
</TableHeader>
1883+
<TableBody items={items} loadingState={loadingState}>
1884+
{item => (
1885+
<Row key={item.key}>
1886+
{column => <Cell>{item[column]}</Cell>}
1887+
</Row>
1888+
)}
1889+
</TableBody>
1890+
</TableView>
1891+
);
1892+
}

packages/@react-spectrum/table/test/Table.test.js

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,8 @@ let {
3838
InlineDeleteButtons: DeletableRowsTable,
3939
EmptyStateStory: EmptyStateTable,
4040
WithBreadcrumbNavigation: TableWithBreadcrumbs,
41-
TypeaheadWithDialog: TypeaheadWithDialog
41+
TypeaheadWithDialog: TypeaheadWithDialog,
42+
ColumnHeaderFocusRingTable
4243
} = composeStories(stories);
4344

4445

@@ -185,6 +186,9 @@ export let tableTests = () => {
185186
return el;
186187
};
187188

189+
let focusCell = (tree, text) => act(() => getCell(tree, text).focus());
190+
let moveFocus = (key, opts = {}) => {fireEvent.keyDown(document.activeElement, {key, ...opts});};
191+
188192
it('renders a static table', function () {
189193
let {getByRole} = render(
190194
<TableView aria-label="Table" data-testid="test">
@@ -926,9 +930,6 @@ export let tableTests = () => {
926930
</TableView>
927931
);
928932

929-
let focusCell = (tree, text) => act(() => getCell(tree, text).focus());
930-
let moveFocus = (key, opts = {}) => {fireEvent.keyDown(document.activeElement, {key, ...opts});};
931-
932933
describe('ArrowRight', function () {
933934
it('should move focus to the next cell in a row with ArrowRight', function () {
934935
let tree = renderTable();
@@ -4449,14 +4450,27 @@ export let tableTests = () => {
44494450
await act(() => Promise.resolve());
44504451
let table = tree.getByRole('grid');
44514452
let header = within(table).getAllByRole('columnheader')[2];
4452-
expect(header).not.toHaveAttribute('tabindex');
4453+
expect(header).toHaveAttribute('tabindex', '-1');
44534454
let headerButton = within(header).getByRole('button');
44544455
expect(headerButton).toHaveAttribute('aria-disabled', 'true');
4455-
// Can't progamatically focus the column headers since they have no tab index when table is empty
4456-
act(() => {
4457-
header.focus();
4458-
});
4459-
expect(document.activeElement).toBe(document.body);
4456+
});
4457+
4458+
it('should shift focus to the table if table becomes empty via column sort', function () {
4459+
let tree = render(<ColumnHeaderFocusRingTable />);
4460+
let rows = tree.getAllByRole('row');
4461+
expect(rows).toHaveLength(3);
4462+
focusCell(tree, 'Height');
4463+
expect(document.activeElement).toHaveTextContent('Height');
4464+
fireEvent.keyDown(document.activeElement, {key: 'Enter'});
4465+
fireEvent.keyUp(document.activeElement, {key: 'Enter'});
4466+
act(() => jest.advanceTimersByTime(500));
4467+
let table = tree.getByRole('grid');
4468+
expect(document.activeElement).toBe(table);
4469+
// Run the rest of the timeout and run the transitions
4470+
act(() => {jest.runAllTimers();});
4471+
act(() => {jest.runAllTimers();});
4472+
rows = tree.getAllByRole('row');
4473+
expect(rows).toHaveLength(2);
44604474
});
44614475

44624476
it('should disable press interactions with the column headers', async function () {

0 commit comments

Comments
 (0)