Skip to content

Commit 9498c7d

Browse files
authored
Stop propagation when pressing Enter on a native link (#3380)
1 parent c0b2454 commit 9498c7d

File tree

2 files changed

+61
-8
lines changed

2 files changed

+61
-8
lines changed

packages/@react-aria/interactions/src/usePress.ts

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ export function usePress(props: PressHookProps): PressResult {
228228

229229
let pressProps: DOMAttributes = {
230230
onKeyDown(e) {
231-
if (isValidKeyboardEvent(e.nativeEvent) && e.currentTarget.contains(e.target as Element)) {
231+
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && e.currentTarget.contains(e.target as Element)) {
232232
if (shouldPreventDefaultKeyboard(e.target as Element)) {
233233
e.preventDefault();
234234
}
@@ -246,10 +246,15 @@ export function usePress(props: PressHookProps): PressResult {
246246
// instead of the same element where the key down event occurred.
247247
addGlobalListener(document, 'keyup', onKeyUp, false);
248248
}
249+
} else if (e.key === 'Enter' && isHTMLAnchorLink(e.currentTarget)) {
250+
// If the target is a link, we won't have handled this above because we want the default
251+
// browser behavior to open the link when pressing Enter. But we still need to prevent
252+
// default so that elements above do not also handle it (e.g. table row).
253+
e.stopPropagation();
249254
}
250255
},
251256
onKeyUp(e) {
252-
if (isValidKeyboardEvent(e.nativeEvent) && !e.repeat && e.currentTarget.contains(e.target as Element)) {
257+
if (isValidKeyboardEvent(e.nativeEvent, e.currentTarget) && !e.repeat && e.currentTarget.contains(e.target as Element)) {
253258
triggerPressUp(createEvent(state.target, e), 'keyboard');
254259
}
255260
},
@@ -284,7 +289,7 @@ export function usePress(props: PressHookProps): PressResult {
284289
};
285290

286291
let onKeyUp = (e: KeyboardEvent) => {
287-
if (state.isPressed && isValidKeyboardEvent(e)) {
292+
if (state.isPressed && isValidKeyboardEvent(e, state.target)) {
288293
if (shouldPreventDefaultKeyboard(e.target as Element)) {
289294
e.preventDefault();
290295
}
@@ -297,7 +302,7 @@ export function usePress(props: PressHookProps): PressResult {
297302

298303
// If the target is a link, trigger the click method to open the URL,
299304
// but defer triggering pressEnd until onClick event handler.
300-
if (state.target instanceof HTMLElement && (state.target.contains(target) && isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link')) {
305+
if (state.target instanceof HTMLElement && state.target.contains(target) && (isHTMLAnchorLink(state.target) || state.target.getAttribute('role') === 'link')) {
301306
state.target.click();
302307
}
303308
}
@@ -662,13 +667,13 @@ export function usePress(props: PressHookProps): PressResult {
662667
};
663668
}
664669

665-
function isHTMLAnchorLink(target: HTMLElement): boolean {
670+
function isHTMLAnchorLink(target: Element): boolean {
666671
return target.tagName === 'A' && target.hasAttribute('href');
667672
}
668673

669-
function isValidKeyboardEvent(event: KeyboardEvent): boolean {
670-
const {key, code, target} = event;
671-
const element = target as HTMLElement;
674+
function isValidKeyboardEvent(event: KeyboardEvent, currentTarget: Element): boolean {
675+
const {key, code} = event;
676+
const element = currentTarget as HTMLElement;
672677
const {tagName, isContentEditable} = element;
673678
const role = element.getAttribute('role');
674679
// Accessibility for keyboards. Space and Enter only.

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

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1822,6 +1822,54 @@ describe('TableView', function () {
18221822
let checkbox = tree.getByLabelText('Select All');
18231823
expect(checkbox.checked).toBeFalsy();
18241824
});
1825+
1826+
describe('Space key with focus on a link within a cell', () => {
1827+
it('should toggle selection and prevent scrolling of the table', () => {
1828+
let tree = render(
1829+
<TableView aria-label="Table" selectionMode="multiple">
1830+
<TableHeader columns={columns}>
1831+
{column => <Column>{column.name}</Column>}
1832+
</TableHeader>
1833+
<TableBody items={items}>
1834+
{item =>
1835+
(<Row key={item.foo}>
1836+
{key => <Cell><Link><a href={`https://example.com/?id=${item.id}`} target="_blank">{item[key]}</a></Link></Cell>}
1837+
</Row>)
1838+
}
1839+
</TableBody>
1840+
</TableView>
1841+
);
1842+
1843+
let row = tree.getAllByRole('row')[1];
1844+
expect(row).toHaveAttribute('aria-selected', 'false');
1845+
1846+
let link = within(row).getAllByRole('link')[0];
1847+
expect(link.textContent).toBe('Foo 1');
1848+
1849+
act(() => {
1850+
link.focus();
1851+
fireEvent.keyDown(link, {key: ' '});
1852+
fireEvent.keyUp(link, {key: ' '});
1853+
jest.runAllTimers();
1854+
});
1855+
1856+
row = tree.getAllByRole('row')[1];
1857+
expect(row).toHaveAttribute('aria-selected', 'true');
1858+
1859+
act(() => {
1860+
link.focus();
1861+
fireEvent.keyDown(link, {key: ' '});
1862+
fireEvent.keyUp(link, {key: ' '});
1863+
jest.runAllTimers();
1864+
});
1865+
1866+
row = tree.getAllByRole('row')[1];
1867+
link = within(row).getAllByRole('link')[0];
1868+
1869+
expect(row).toHaveAttribute('aria-selected', 'false');
1870+
expect(link.textContent).toBe('Foo 1');
1871+
});
1872+
});
18251873
});
18261874

18271875
describe('range selection', function () {

0 commit comments

Comments
 (0)