Skip to content

Commit c02ede9

Browse files
authored
Adding support to SelectionManager for ctrl-clicking on windows (#2557)
1 parent cda530d commit c02ede9

File tree

6 files changed

+105
-17
lines changed

6 files changed

+105
-17
lines changed

packages/@react-aria/selection/src/useSelectableCollection.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,12 @@
1313
import {FocusEvent, HTMLAttributes, Key, KeyboardEvent, RefObject, useEffect, useRef} from 'react';
1414
import {focusSafely, getFocusableTreeWalker} from '@react-aria/focus';
1515
import {FocusStrategy, KeyboardDelegate} from '@react-types/shared';
16-
import {focusWithoutScrolling, isMac, mergeProps, useEvent} from '@react-aria/utils';
17-
import {isNonContiguousSelectionModifier} from './utils';
16+
import {focusWithoutScrolling, mergeProps, useEvent} from '@react-aria/utils';
17+
import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
1818
import {MultipleSelectionManager} from '@react-stately/selection';
1919
import {useLocale} from '@react-aria/i18n';
2020
import {useTypeSelect} from './useTypeSelect';
2121

22-
function isCtrlKeyPressed(e: KeyboardEvent) {
23-
if (isMac()) {
24-
return e.metaKey;
25-
}
26-
27-
return e.ctrlKey;
28-
}
29-
3022
interface SelectableCollectionOptions {
3123
/**
3224
* An interface for reading and updating multiple selection state.

packages/@react-aria/selection/src/useSelectableItem.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {focusSafely} from '@react-aria/focus';
1414
import {HTMLAttributes, Key, RefObject, useEffect, useRef} from 'react';
15-
import {isNonContiguousSelectionModifier} from './utils';
15+
import {isCtrlKeyPressed, isNonContiguousSelectionModifier} from './utils';
1616
import {LongPressEvent, PressEvent} from '@react-types/shared';
1717
import {mergeProps} from '@react-aria/utils';
1818
import {MultipleSelectionManager} from '@react-stately/selection';
@@ -86,7 +86,24 @@ export function useSelectableItem(options: SelectableItemOptions): SelectableIte
8686
if (e.pointerType === 'keyboard' && isNonContiguousSelectionModifier(e)) {
8787
manager.toggleSelection(key);
8888
} else {
89-
manager.select(key, e);
89+
if (manager.selectionMode === 'none') {
90+
return;
91+
}
92+
93+
if (manager.selectionMode === 'single') {
94+
if (manager.isSelected(key) && !manager.disallowEmptySelection) {
95+
manager.toggleSelection(key);
96+
} else {
97+
manager.replaceSelection(key);
98+
}
99+
} else if (e && e.shiftKey) {
100+
manager.extendSelection(key);
101+
} else if (manager.selectionBehavior === 'toggle' || (e && (isCtrlKeyPressed(e) || e.pointerType === 'touch' || e.pointerType === 'virtual'))) {
102+
// if touch or virtual (VO) then we just want to toggle, otherwise it's impossible to multi select because they don't have modifier keys
103+
manager.toggleSelection(key);
104+
} else {
105+
manager.replaceSelection(key);
106+
}
90107
}
91108
};
92109

packages/@react-aria/selection/src/utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,24 @@
1111
*/
1212

1313
import {isAppleDevice} from '@react-aria/utils';
14+
import {isMac} from '@react-aria/utils';
1415

1516
interface Event {
1617
altKey: boolean,
17-
ctrlKey: boolean
18+
ctrlKey: boolean,
19+
metaKey: boolean
1820
}
1921

2022
export function isNonContiguousSelectionModifier(e: Event) {
2123
// Ctrl + Arrow Up/Arrow Down has a system wide meaning on macOS, so use Alt instead.
2224
// On Windows and Ubuntu, Alt + Space has a system wide meaning.
2325
return isAppleDevice() ? e.altKey : e.ctrlKey;
2426
}
27+
28+
export function isCtrlKeyPressed(e: Event) {
29+
if (isMac()) {
30+
return e.metaKey;
31+
}
32+
33+
return e.ctrlKey;
34+
}

packages/@react-spectrum/list/test/ListView.test.js

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -387,13 +387,81 @@ describe('ListView', function () {
387387
expect(rows[2]).toHaveAttribute('aria-selected', 'true');
388388
});
389389

390-
it('should support single tap to perform row selection with screen reader if onAction isn\'t provided', function () {
390+
it('should toggle items in selection highlight with ctrl-click on Mac', function () {
391+
let uaMock = jest.spyOn(navigator, 'platform', 'get').mockImplementation(() => 'Mac');
391392
let onSelectionChange = jest.fn();
392393
let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', selectionStyle: 'highlight'});
393394

394395
let rows = tree.getAllByRole('row');
395396
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
397+
expect(rows[2]).toHaveAttribute('aria-selected', 'false');
398+
act(() => userEvent.click(getCell(tree, 'Bar'), {ctrlKey: true}));
396399

400+
checkSelection(onSelectionChange, ['bar']);
401+
expect(rows[1]).toHaveAttribute('aria-selected', 'true');
402+
403+
onSelectionChange.mockClear();
404+
act(() => userEvent.click(getCell(tree, 'Baz'), {ctrlKey: true}));
405+
checkSelection(onSelectionChange, ['baz']);
406+
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
407+
expect(rows[2]).toHaveAttribute('aria-selected', 'true');
408+
409+
uaMock.mockRestore();
410+
});
411+
412+
it('should allow multiple items to be selected in selection highlight with ctrl-click on Windows', function () {
413+
let uaMock = jest.spyOn(navigator, 'userAgent', 'get').mockImplementation(() => 'Windows');
414+
let onSelectionChange = jest.fn();
415+
let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', selectionStyle: 'highlight'});
416+
417+
let rows = tree.getAllByRole('row');
418+
expect(rows[0]).toHaveAttribute('aria-selected', 'false');
419+
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
420+
expect(rows[2]).toHaveAttribute('aria-selected', 'false');
421+
act(() => userEvent.click(getCell(tree, 'Foo'), {ctrlKey: true}));
422+
423+
checkSelection(onSelectionChange, ['foo']);
424+
expect(rows[0]).toHaveAttribute('aria-selected', 'true');
425+
426+
onSelectionChange.mockClear();
427+
act(() => userEvent.click(getCell(tree, 'Baz'), {ctrlKey: true}));
428+
checkSelection(onSelectionChange, ['foo', 'baz']);
429+
expect(rows[0]).toHaveAttribute('aria-selected', 'true');
430+
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
431+
expect(rows[2]).toHaveAttribute('aria-selected', 'true');
432+
433+
uaMock.mockRestore();
434+
});
435+
436+
it('should toggle items in selection highlight with meta-click on Windows', function () {
437+
let uaMock = jest.spyOn(navigator, 'userAgent', 'get').mockImplementation(() => 'Windows');
438+
let onSelectionChange = jest.fn();
439+
let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', selectionStyle: 'highlight'});
440+
441+
let rows = tree.getAllByRole('row');
442+
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
443+
expect(rows[2]).toHaveAttribute('aria-selected', 'false');
444+
act(() => userEvent.click(getCell(tree, 'Bar'), {metaKey: true}));
445+
446+
checkSelection(onSelectionChange, ['bar']);
447+
expect(rows[1]).toHaveAttribute('aria-selected', 'true');
448+
449+
onSelectionChange.mockClear();
450+
act(() => userEvent.click(getCell(tree, 'Baz'), {metaKey: true}));
451+
checkSelection(onSelectionChange, ['baz']);
452+
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
453+
expect(rows[2]).toHaveAttribute('aria-selected', 'true');
454+
455+
uaMock.mockRestore();
456+
});
457+
458+
it('should support single tap to perform row selection with screen reader if onAction isn\'t provided', function () {
459+
let onSelectionChange = jest.fn();
460+
let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', selectionStyle: 'highlight'});
461+
462+
let rows = tree.getAllByRole('row');
463+
expect(rows[1]).toHaveAttribute('aria-selected', 'false');
464+
397465
act(() => userEvent.click(within(rows[1]).getByText('Bar'), {pointerType: 'touch', width: 0, height: 0}));
398466
checkSelection(onSelectionChange, [
399467
'bar'

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2414,6 +2414,7 @@ describe('TableView', function () {
24142414
});
24152415

24162416
it('will add to the current selection if the command key is pressed', function () {
2417+
let uaMock = jest.spyOn(navigator, 'platform', 'get').mockImplementation(() => 'Mac');
24172418
let onSelectionChange = jest.fn();
24182419
let tree = renderTable({onSelectionChange, selectionStyle: 'highlight'});
24192420

@@ -2439,6 +2440,8 @@ describe('TableView', function () {
24392440
checkRowSelection(rows.slice(6, 10), false);
24402441
checkRowSelection(rows.slice(10, 21), true);
24412442
checkRowSelection(rows.slice(21), false);
2443+
2444+
uaMock.mockRestore();
24422445
});
24432446

24442447
it('should toggle selection with touch', function () {

packages/@react-stately/selection/src/SelectionManager.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -422,9 +422,7 @@ export class SelectionManager implements MultipleSelectionManager {
422422
} else {
423423
this.replaceSelection(key);
424424
}
425-
} else if (e && e.shiftKey) {
426-
this.extendSelection(key);
427-
} else if (this.selectionBehavior === 'toggle' || (e && (e.metaKey || e.pointerType === 'touch' || e.pointerType === 'virtual'))) {
425+
} else if (this.selectionBehavior === 'toggle' || (e && (e.pointerType === 'touch' || e.pointerType === 'virtual'))) {
428426
// if touch or virtual (VO) then we just want to toggle, otherwise it's impossible to multi select because they don't have modifier keys
429427
this.toggleSelection(key);
430428
} else {

0 commit comments

Comments
 (0)