Skip to content

Commit c1a9b03

Browse files
authored
fix: Ensure that combobox empty state is rendered when items arent directly provided to the combobox (#8720)
* fix: Ensure that combobox empty state is rendered when items arent directly provided to the combobox * updating docs
1 parent 3e8cdab commit c1a9b03

File tree

6 files changed

+45
-13
lines changed

6 files changed

+45
-13
lines changed

packages/@react-spectrum/s2/src/ComboBox.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -339,14 +339,13 @@ export const ComboBox = /*#__PURE__*/ (forwardRef as forwardRefType)(function Co
339339
labelPosition = 'top',
340340
UNSAFE_className = '',
341341
UNSAFE_style,
342-
loadingState,
343342
...comboBoxProps
344343
} = props;
345344

346345
return (
347346
<AriaComboBox
348347
{...comboBoxProps}
349-
allowsEmptyCollection={loadingState != null}
348+
allowsEmptyCollection
350349
style={UNSAFE_style}
351350
className={UNSAFE_className + style(field(), getAllowedOverrides())({
352351
isInForm: !!formContext,

packages/@react-spectrum/s2/test/Combobox.test.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,10 @@ describe('Combobox', () => {
6464
comboboxTester.setInteractionType('mouse');
6565
await comboboxTester.open();
6666

67-
expect(comboboxTester.options()).toHaveLength(1);
67+
let options = comboboxTester.options();
68+
expect(options).toHaveLength(1);
69+
expect(comboboxTester.listbox).toBeTruthy();
70+
expect(options[0]).toHaveTextContent('No results');
6871
expect(within(comboboxTester.listbox!).getByTestId('loadMoreSentinel')).toBeInTheDocument();
6972
});
7073

packages/@react-stately/list/src/ListCollection.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ export class ListCollection<T> implements Collection<Node<T>> {
2222
this.iterable = nodes;
2323

2424
let visit = (node: Node<T>) => {
25-
this.keyMap.set(node.key, node);
26-
27-
if (node.childNodes && node.type === 'section') {
28-
for (let child of node.childNodes) {
29-
visit(child);
25+
// Skip the loader node so it isn't added to the keymap and thus
26+
// doesn't influence the size count. This should only matter for RAC and S2
27+
if (node.type !== 'loader') {
28+
this.keyMap.set(node.key, node);
29+
30+
if (node.childNodes && node.type === 'section') {
31+
for (let child of node.childNodes) {
32+
visit(child);
33+
}
3034
}
3135
}
3236
};

packages/react-aria-components/docs/ComboBox.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,7 @@ interface MyComboBoxProps<T extends object> extends Omit<ComboBoxProps<T>, 'chil
335335

336336
function MyComboBox<T extends object>({label, description, errorMessage, children, ...props}: MyComboBoxProps<T>) {
337337
return (
338-
<ComboBox {...props}>
338+
<ComboBox allowsEmptyCollection {...props}>
339339
<Label>{label}</Label>
340340
<div className="my-combobox-container">
341341
<Input />
@@ -344,7 +344,7 @@ function MyComboBox<T extends object>({label, description, errorMessage, childre
344344
{description && <Text slot="description">{description}</Text>}
345345
<FieldError>{errorMessage}</FieldError>
346346
<Popover>
347-
<ListBox>
347+
<ListBox renderEmptyState={() => <div className="my-item">No results found</div>}>
348348
{children}
349349
</ListBox>
350350
</Popover>

packages/react-aria-components/stories/ComboBox.stories.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export type ComboBoxStory = StoryFn<typeof ComboBox>;
2828
export type ComboBoxStoryObj = StoryObj<typeof ComboBox>;
2929

3030
export const ComboBoxExample: ComboBoxStory = () => (
31-
<ComboBox name="combo-box-example" data-testid="combo-box-example">
31+
<ComboBox name="combo-box-example" data-testid="combo-box-example" allowsEmptyCollection>
3232
<Label style={{display: 'block'}}>Test</Label>
3333
<div style={{display: 'flex'}}>
3434
<Input />
@@ -38,12 +38,14 @@ export const ComboBoxExample: ComboBoxStory = () => (
3838
</div>
3939
<Popover placement="bottom end">
4040
<ListBox
41+
renderEmptyState={renderEmptyState}
4142
data-testid="combo-box-list-box"
4243
className={styles.menu}>
4344
<MyListBoxItem>Foo</MyListBoxItem>
4445
<MyListBoxItem>Bar</MyListBoxItem>
4546
<MyListBoxItem>Baz</MyListBoxItem>
4647
<MyListBoxItem href="http://google.com">Google</MyListBoxItem>
48+
<MyListBoxLoaderIndicator />
4749
</ListBox>
4850
</Popover>
4951
</ComboBox>

packages/react-aria-components/test/ComboBox.test.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,18 @@
1111
*/
1212

1313
import {act} from '@testing-library/react';
14-
import {Button, ComboBox, ComboBoxContext, FieldError, Header, Input, Label, ListBox, ListBoxItem, ListBoxSection, ListLayout, Popover, Text, Virtualizer} from '../';
14+
import {Button, ComboBox, ComboBoxContext, FieldError, Header, Input, Label, ListBox, ListBoxItem, ListBoxLoadMoreItem, ListBoxSection, ListLayout, Popover, Text, Virtualizer} from '../';
1515
import {fireEvent, pointerMap, render, within} from '@react-spectrum/test-utils-internal';
1616
import React from 'react';
1717
import {User} from '@react-aria/test-utils';
1818
import userEvent from '@testing-library/user-event';
1919

20+
let renderEmptyState = () => {
21+
return (
22+
<div>No results</div>
23+
);
24+
};
25+
2026
let TestComboBox = (props) => (
2127
<ComboBox name="test-combobox" defaultInputValue="C" data-foo="bar" {...props}>
2228
<Label>Favorite Animal</Label>
@@ -25,10 +31,13 @@ let TestComboBox = (props) => (
2531
<Text slot="description">Description</Text>
2632
<Text slot="errorMessage">Error</Text>
2733
<Popover>
28-
<ListBox>
34+
<ListBox renderEmptyState={renderEmptyState}>
2935
<ListBoxItem id="1">Cat</ListBoxItem>
3036
<ListBoxItem id="2">Dog</ListBoxItem>
3137
<ListBoxItem id="3">Kangaroo</ListBoxItem>
38+
<ListBoxLoadMoreItem>
39+
loading
40+
</ListBoxLoadMoreItem>
3241
</ListBox>
3342
</Popover>
3443
</ComboBox>
@@ -384,4 +393,19 @@ describe('ComboBox', () => {
384393
let input = getByRole('combobox');
385394
expect(input).toHaveAttribute('form', 'test');
386395
});
396+
397+
it('should render empty state even when there is a loader provided and allowsEmptyCollection is true', async () => {
398+
let tree = render(<TestComboBox allowsEmptyCollection />);
399+
400+
let comboboxTester = testUtilUser.createTester('ComboBox', {root: tree.container});
401+
act(() => {
402+
comboboxTester.combobox.focus();
403+
});
404+
await user.keyboard('p');
405+
406+
let options = comboboxTester.options();
407+
expect(options).toHaveLength(1);
408+
expect(comboboxTester.listbox).toBeTruthy();
409+
expect(options[0]).toHaveTextContent('No results');
410+
});
387411
});

0 commit comments

Comments
 (0)