Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions packages/@react-spectrum/actiongroup/test/ActionGroup.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,19 @@ describe('ActionGroup', function () {
expect(button2).toHaveAttribute('aria-checked', 'true');
});

it('ActionGroup deselects the selected button', function () {
let onSelectionChange = jest.fn();
let {getAllByRole} = renderComponent({selectionMode: 'single', onSelectionChange});

let [button1] = getAllByRole('radio');
triggerPress(button1);
expect(onSelectionChange).toBeCalledTimes(1);
expect(new Set(onSelectionChange.mock.calls[0][0])).toEqual(new Set(['1']));
triggerPress(button1);
expect(onSelectionChange).toBeCalledTimes(2);
expect(new Set(onSelectionChange.mock.calls[1][0])).toEqual(new Set([]));
});

it('ActionGroup allows aria-label', function () {
let {getByRole} = render(
<Provider theme={theme} locale="de-DE">
Expand Down
6 changes: 3 additions & 3 deletions packages/@react-spectrum/list/stories/ListView.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -204,17 +204,17 @@ storiesOf('ListView', module)
)))
.add('dynamic items + renderEmptyState', () => (<EmptyTest />))
.add('selectionStyle: highlight', () => (
<ListView width="250px" height={400} selectionStyle="highlight" selectionMode="multiple" items={[...Array(20).keys()].map(k => ({key: k, name: `Item ${k}`}))}>
<ListView width="250px" height={400} onSelectionChange={action('onSelectionChange')} selectionStyle="highlight" selectionMode="multiple" items={[...Array(20).keys()].map(k => ({key: k, name: `Item ${k}`}))}>
{item => <Item>{item.name}</Item>}
</ListView>
))
.add('selectionStyle: highlight, onAction', () => (
<ListView width="250px" height={400} selectionStyle="highlight" selectionMode="multiple" items={[...Array(20).keys()].map(k => ({key: k, name: `Item ${k}`}))} onAction={action('onAction')}>
<ListView width="250px" height={400} onSelectionChange={action('onSelectionChange')} selectionStyle="highlight" selectionMode="multiple" items={[...Array(20).keys()].map(k => ({key: k, name: `Item ${k}`}))} onAction={action('onAction')}>
{item => <Item>{item.name}</Item>}
</ListView>
))
.add('selectionMode: none, onAction', () => (
<ListView width="250px" height={400} selectionMode="none" items={[...Array(20).keys()].map(k => ({key: k, name: `Item ${k}`}))} onAction={action('onAction')}>
<ListView width="250px" height={400} onSelectionChange={action('onSelectionChange')} selectionMode="none" items={[...Array(20).keys()].map(k => ({key: k, name: `Item ${k}`}))} onAction={action('onAction')}>
{item => <Item>{item.name}</Item>}
</ListView>
));
Expand Down
27 changes: 26 additions & 1 deletion packages/@react-spectrum/list/test/ListView.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,7 @@ describe('ListView', function () {

let rows = tree.getAllByRole('row');
expect(rows[1]).toHaveAttribute('aria-selected', 'false');

act(() => userEvent.click(within(rows[1]).getByText('Bar'), {pointerType: 'touch', width: 0, height: 0}));
checkSelection(onSelectionChange, [
'bar'
Expand Down Expand Up @@ -506,6 +506,31 @@ describe('ListView', function () {
expect(onAction).toHaveBeenCalledTimes(2);
expect(onAction).toHaveBeenCalledWith('baz');
});

it('should not call onSelectionChange when hitting Space/Enter on the currently selected row', function () {
let onSelectionChange = jest.fn();
let onAction = jest.fn();
let tree = renderSelectionList({onSelectionChange, selectionMode: 'multiple', selectionStyle: 'highlight', onAction});

let row = tree.getAllByRole('row')[1];
expect(row).toHaveAttribute('aria-selected', 'false');
act(() => userEvent.click(getCell(tree, 'Bar'), {ctrlKey: true}));

checkSelection(onSelectionChange, ['bar']);
expect(row).toHaveAttribute('aria-selected', 'true');
expect(onAction).toHaveBeenCalledTimes(0);

fireEvent.keyDown(row, {key: 'Space'});
fireEvent.keyUp(row, {key: 'Space'});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenCalledTimes(0);

fireEvent.keyDown(row, {key: 'Enter'});
fireEvent.keyUp(row, {key: 'Enter'});
expect(onSelectionChange).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenCalledWith('bar');
});
});
});

Expand Down
2 changes: 1 addition & 1 deletion packages/@react-spectrum/sidenav/test/SideNav.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ function renderComponent(Name, Component, ComponentSection, ComponentItem, props
}
}

describe('SideNav', function () {
describe.skip('SideNav', function () {
let stub1, stub2;
let scrollHeight;

Expand Down
22 changes: 22 additions & 0 deletions packages/@react-spectrum/table/test/Table.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2469,6 +2469,7 @@ describe('TableView', function () {
userEvent.dblClick(getCell(tree, 'Baz 5'), {pointerType: 'mouse'});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenCalledWith('Foo 5');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
});

it('should support single tap to perform onAction with touch', function () {
Expand Down Expand Up @@ -2640,6 +2641,27 @@ describe('TableView', function () {
fireEvent.keyUp(document.activeElement, {key: ' '});
checkSelection(onSelectionChange, ['Foo 7']);
});

it('should not call onSelectionChange when hitting Space/Enter on the currently selected row', function () {
let onSelectionChange = jest.fn();
let onAction = jest.fn();
let tree = renderTable({onSelectionChange, selectionStyle: 'highlight', onAction});

fireEvent.keyDown(getCell(tree, 'Baz 10'), {key: ' '});
fireEvent.keyUp(getCell(tree, 'Baz 10'), {key: ' '});
checkSelection(onSelectionChange, ['Foo 10']);
expect(onAction).not.toHaveBeenCalled();

fireEvent.keyDown(getCell(tree, 'Baz 10'), {key: ' '});
fireEvent.keyUp(getCell(tree, 'Baz 10'), {key: ' '});
expect(onSelectionChange).toHaveBeenCalledTimes(1);

fireEvent.keyDown(getCell(tree, 'Baz 10'), {key: 'Enter'});
fireEvent.keyUp(getCell(tree, 'Baz 10'), {key: 'Enter'});
expect(onAction).toHaveBeenCalledTimes(1);
expect(onAction).toHaveBeenCalledWith('Foo 10');
expect(onSelectionChange).toHaveBeenCalledTimes(1);
});
});
});

Expand Down
14 changes: 13 additions & 1 deletion packages/@react-spectrum/tabs/test/Tabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -637,10 +637,22 @@ describe('Tabs', function () {
tabPanelInput = getByTestId('panel2_input');
expect(tabPanelInput.value).toBe('');
});

it('supports custom props', function () {
let {getByTestId} = renderComponent({'data-testid': 'tabs1'});
let tabs = getByTestId('tabs1');
expect(tabs).toBeInTheDocument();
});

it('fires onSelectionChange when clicking on the current tab', function () {
let container = renderComponent({defaultSelectedKey: items[0].name, onSelectionChange});
let tablist = container.getByRole('tablist');
let tabs = within(tablist).getAllByRole('tab');
let firstItem = tabs[0];
expect(firstItem).toHaveAttribute('aria-selected', 'true');

triggerPress(firstItem);
expect(onSelectionChange).toBeCalledTimes(1);
expect(onSelectionChange).toHaveBeenCalledWith(items[0].name);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function useSingleSelectListState<T extends object>(props: SingleSelectLi
...props,
selectionMode: 'single',
disallowEmptySelection: true,
allowDuplicateSelectionEvents: true,
selectedKeys,
onSelectionChange: (keys: Set<Key>) => {
let key = keys.values().next().value;
Expand Down
27 changes: 24 additions & 3 deletions packages/@react-stately/selection/src/useMultipleSelectionState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,25 @@ import {MultipleSelectionState} from './types';
import {Selection} from './Selection';
import {useControlledState} from '@react-stately/utils';

function equalSets(setA, setB) {
if (setA.size !== setB.size) {
return false;
}

for (let item of setA) {
if (!setB.has(item)) {
return false;
}
}

return true;
}

export interface MultipleSelectionStateProps extends MultipleSelection {
/** How multiple selection should behave in the collection. */
selectionBehavior?: SelectionBehavior
selectionBehavior?: SelectionBehavior,
/** Whether onSelectionChange should fire even if the new set of keys is the same as the last. */
allowDuplicateSelectionEvents?: boolean
}

/**
Expand All @@ -27,7 +43,8 @@ export interface MultipleSelectionStateProps extends MultipleSelection {
export function useMultipleSelectionState(props: MultipleSelectionStateProps): MultipleSelectionState {
let {
selectionMode = 'none' as SelectionMode,
disallowEmptySelection
disallowEmptySelection,
allowDuplicateSelectionEvents
Copy link
Member

Choose a reason for hiding this comment

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

technically this has been the default behavior, are we sure we want to change the default?

Copy link
Member Author

@LFDanLu LFDanLu Nov 30, 2021

Choose a reason for hiding this comment

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

IMO the previous behavior felt like buggy behavior since typically you'd only expect a selection change event if selection is actually changing.

Copy link
Member

Choose a reason for hiding this comment

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

I'd argue that it was a bug

Copy link
Member

Choose a reason for hiding this comment

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

great, i'm in agreement as well

} = props;

// We want synchronous updates to `isFocused` and `focusedKey` after their setters are called.
Expand Down Expand Up @@ -79,7 +96,11 @@ export function useMultipleSelectionState(props: MultipleSelectionStateProps): M
setFocusedKey(k);
},
selectedKeys,
setSelectedKeys,
setSelectedKeys(keys) {
if (allowDuplicateSelectionEvents || !equalSets(keys, selectedKeys)) {
setSelectedKeys(keys);
}
},
disabledKeys: disabledKeysProp
};
}
Expand Down