Skip to content

Commit 6066c6c

Browse files
committed
fix tests and pass submenutrigger node to filterFn
1 parent 19b695e commit 6066c6c

File tree

3 files changed

+87
-22
lines changed

3 files changed

+87
-22
lines changed

packages/react-aria-components/src/Menu.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,9 +116,9 @@ class SubmenuTriggerNode<T> extends CollectionNode<T> {
116116

117117
filter(collection: BaseCollection<T>, newCollection: BaseCollection<T>, filterFn: (textValue: string, node: Node<T>) => boolean): CollectionNode<T> | null {
118118
let triggerNode = collection.getItem(this.firstChildKey!);
119-
if (triggerNode && filterFn(triggerNode.textValue, triggerNode)) {
120-
// TODO: perhaps should call super.filter for correctness, but basically add the menu item child of the submenutrigger
121-
// to the keymap so it renders
119+
// Note that this provides the SubmenuTrigger node rather than the MenuItemNode it wraps to the filter function. Probably more useful
120+
// because that node has the proper parentKey information (aka the section if any, the menu item will just point to the SubmenuTrigger node)
121+
if (triggerNode && filterFn(triggerNode.textValue, this)) {
122122
newCollection.addNode(triggerNode as CollectionNode<T>);
123123
return this.clone();
124124
}

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,17 @@
1212

1313
import {action} from '@storybook/addon-actions';
1414
import {Autocomplete, Button, Cell, Collection, Column, DialogTrigger, GridList, Header, Input, Keyboard, Label, ListBox, ListBoxSection, ListLayout, Menu, MenuItem, MenuSection, MenuTrigger, OverlayArrow, Popover, Row, SearchField, Select, SelectValue, Separator, SubmenuTrigger, Table, TableBody, TableHeader, TableLayout, TagGroup, TagList, Text, TextField, Tooltip, TooltipTrigger, Virtualizer} from 'react-aria-components';
15+
import {LoadingSpinner, MyListBoxItem, MyMenuItem} from './utils';
1516
import {Meta, StoryObj} from '@storybook/react';
1617
import {MyCheckbox} from './Table.stories';
17-
import {MyListBoxItem, MyMenuItem} from './utils';
18-
import {MyListBoxLoaderIndicator, renderEmptyState} from './ListBox.stories';
18+
import {MyGridListItem} from './GridList.stories';
19+
import {MyListBoxLoaderIndicator} from './ListBox.stories';
1920
import {MyTag} from './TagGroup.stories';
20-
import React from 'react';
21+
import React, {useState} from 'react';
2122
import styles from '../example/index.css';
2223
import {useAsyncList, useListData, useTreeData} from 'react-stately';
2324
import {useFilter} from 'react-aria';
2425
import './styles.css';
25-
import {MyGridListItem} from './GridList.stories';
2626

2727
export default {
2828
title: 'React Aria Components/Autocomplete',
@@ -868,8 +868,23 @@ interface Character {
868868
birth_year: number
869869
}
870870

871+
let renderEmptyState = (list, cursor) => {
872+
let emptyStateContent;
873+
if (list.loadingState === 'loading') {
874+
emptyStateContent = <LoadingSpinner style={{height: 20, width: 20, transform: 'translate(-50%, -50%)'}} />;
875+
} else if (list.loadingState === 'idle' && !cursor) {
876+
emptyStateContent = 'No results';
877+
}
878+
return (
879+
<div style={{height: 30, width: '100%'}}>
880+
{emptyStateContent}
881+
</div>
882+
);
883+
};
884+
871885

872886
export const AutocompleteWithAsyncListBox = (args) => {
887+
let [cursor, setCursor] = useState(null);
873888
let list = useAsyncList<Character>({
874889
async load({signal, cursor, filterText}) {
875890
if (cursor) {
@@ -879,6 +894,7 @@ export const AutocompleteWithAsyncListBox = (args) => {
879894
await new Promise(resolve => setTimeout(resolve, args.delay));
880895
let res = await fetch(cursor || `https://swapi.py4e.com/api/people/?search=${filterText}`, {signal});
881896
let json = await res.json();
897+
setCursor(json.next);
882898
return {
883899
items: json.results,
884900
cursor: json.next
@@ -913,7 +929,7 @@ export const AutocompleteWithAsyncListBox = (args) => {
913929
display: 'flex'
914930
}}
915931
aria-label="async virtualized listbox"
916-
renderEmptyState={() => renderEmptyState({isLoading: list.isLoading})}>
932+
renderEmptyState={() => renderEmptyState(list, cursor)}>
917933
<Collection items={list.items}>
918934
{(item: Character) => (
919935
<MyListBoxItem
@@ -1075,10 +1091,10 @@ export const AutocompleteWithTagGroup = () => {
10751091
);
10761092
};
10771093

1078-
function AutocompletePreserveFirstSection(args) {
1094+
function AutocompleteNodeFiltering(args) {
10791095
let {contains} = useFilter({sensitivity: 'base'});
10801096
let filter = (textValue, inputValue, node) => {
1081-
if (node.parentKey === 'Section 1') {
1097+
if ((node.parentKey === 'Section 1' && textValue === 'Open View') || (node.parentKey === 'Section 2' && textValue === 'Appearance')) {
10821098
return true;
10831099
}
10841100
return contains(textValue, inputValue);
@@ -1101,6 +1117,11 @@ function AutocompletePreserveFirstSection(args) {
11011117
}
11021118

11031119
export const AutocompletePreserveFirstSectionStory: AutocompleteStory = {
1104-
render: (args) => <AutocompletePreserveFirstSection {...args} />,
1105-
name: 'Autocomplete, never filter first section'
1120+
render: (args) => <AutocompleteNodeFiltering {...args} />,
1121+
name: 'Autocomplete, per node filtering',
1122+
parameters: {
1123+
description: {
1124+
data: 'It should never filter out Open View or Appearance'
1125+
}
1126+
}
11061127
};

packages/react-aria-components/test/Autocomplete.test.tsx

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,10 +282,6 @@ let StaticBreadcrumbs = (props) => (
282282
<Breadcrumb>Baz</Breadcrumb>
283283
</Breadcrumbs>
284284
);
285-
// TODO: add GridList, Table, TagGroup make sure that it filters and doesn't have virtual focus
286-
// Also test that it doesn't filter Tabs, Tree, Breadcrumbs
287-
// Also test that it can do node specific filtering
288-
289285

290286
let AutocompleteWrapper = ({autocompleteProps = {}, inputProps = {}, children}: {autocompleteProps?: any, inputProps?: any, children?: ReactNode}) => {
291287
let {contains} = useFilter({sensitivity: 'base'});
@@ -362,6 +358,28 @@ let AsyncFiltering = ({autocompleteProps = {}, inputProps = {}}: {autocompletePr
362358
);
363359
};
364360

361+
let CustomFiltering = ({autocompleteProps = {}, inputProps = {}, children}: {autocompleteProps?: any, inputProps?: any, children?: ReactNode}) => {
362+
let [inputValue, setInputValue] = React.useState('');
363+
let {contains} = useFilter({sensitivity: 'base'});
364+
let filter = (textValue, inputValue, node) => {
365+
if (node.parentKey === 'sec1') {
366+
return true;
367+
}
368+
return contains(textValue, inputValue);
369+
};
370+
371+
return (
372+
<Autocomplete inputValue={inputValue} onInputChange={setInputValue} filter={filter} {...autocompleteProps}>
373+
<SearchField {...inputProps}>
374+
<Label style={{display: 'block'}}>Test</Label>
375+
<Input />
376+
<Text style={{display: 'block'}} slot="description">Please select an option below.</Text>
377+
</SearchField>
378+
{children}
379+
</Autocomplete>
380+
);
381+
};
382+
365383
describe('Autocomplete', () => {
366384
let user;
367385
beforeAll(() => {
@@ -896,9 +914,9 @@ describe('Autocomplete', () => {
896914
);
897915

898916
let wrappedComponent = getByTestId('wrapped');
899-
expect(within(wrappedComponent).findByText('Foo')).toBeTruthy();
900-
expect(within(wrappedComponent).findByText('Bar')).toBeTruthy();
901-
expect(within(wrappedComponent).findByText('Baz')).toBeTruthy();
917+
expect(await within(wrappedComponent).findByText('Foo')).toBeTruthy();
918+
expect(await within(wrappedComponent).findByText('Bar')).toBeTruthy();
919+
expect(await within(wrappedComponent).findByText('Baz')).toBeTruthy();
902920

903921
let input = getByRole('searchbox');
904922
await user.tab();
@@ -909,9 +927,35 @@ describe('Autocomplete', () => {
909927
expect(input).not.toHaveAttribute('aria-autocomplete');
910928
expect(input).not.toHaveAttribute('aria-activedescendant');
911929

912-
expect(within(wrappedComponent).findByText('Foo')).toBeTruthy();
913-
expect(within(wrappedComponent).findByText('Bar')).toBeTruthy();
914-
expect(within(wrappedComponent).findByText('Baz')).toBeTruthy();
930+
expect(await within(wrappedComponent).findByText('Foo')).toBeTruthy();
931+
expect(await within(wrappedComponent).findByText('Bar')).toBeTruthy();
932+
expect(await within(wrappedComponent).findByText('Baz')).toBeTruthy();
933+
});
934+
935+
it('should allow user to filter by node information', async () => {
936+
let {getByRole} = render(
937+
<CustomFiltering>
938+
<MenuWithSections />
939+
</CustomFiltering>
940+
);
941+
942+
let input = getByRole('searchbox');
943+
await user.tab();
944+
expect(document.activeElement).toBe(input);
945+
let menu = getByRole('menu');
946+
let sections = within(menu).getAllByRole('group');
947+
expect(sections.length).toBe(2);
948+
let options = within(menu).getAllByRole('menuitem');
949+
expect(options).toHaveLength(6);
950+
951+
await user.keyboard('Copy');
952+
sections = within(menu).getAllByRole('group');
953+
options = within(menu).getAllByRole('menuitem');
954+
expect(options).toHaveLength(4);
955+
expect(within(sections[0]).getByText('Foo')).toBeTruthy();
956+
expect(within(sections[0]).getByText('Bar')).toBeTruthy();
957+
expect(within(sections[0]).getByText('Baz')).toBeTruthy();
958+
expect(within(sections[1]).getByText('Copy')).toBeTruthy();
915959
});
916960
});
917961

0 commit comments

Comments
 (0)