Skip to content

Commit 9acbe32

Browse files
authored
[select] feat(Suggest): sync activeItem with selectedItem on popover close (#3934)
1 parent b7b3a5e commit 9acbe32

File tree

3 files changed

+70
-18
lines changed

3 files changed

+70
-18
lines changed

packages/select/src/components/query-list/queryList.tsx

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ import {
3030
} from "../../common";
3131

3232
export interface IQueryListProps<T> extends IListItemsProps<T> {
33+
/**
34+
* Initial active item, useful if the parent component is controlling its selectedItem but
35+
* not activeItem.
36+
*/
37+
initialActiveItem?: T;
38+
3339
/**
3440
* Callback invoked when user presses a key, after processing `QueryList`'s own key events
3541
* (up/down to navigate active item). This callback is passed to `renderer` and (along with
@@ -171,7 +177,7 @@ export class QueryList<T> extends AbstractComponent2<IQueryListProps<T>, IQueryL
171177
activeItem:
172178
props.activeItem !== undefined
173179
? props.activeItem
174-
: getFirstEnabledItem(filteredItems, props.itemDisabled),
180+
: props.initialActiveItem ?? getFirstEnabledItem(filteredItems, props.itemDisabled),
175181
createNewItem,
176182
filteredItems,
177183
query,
@@ -289,6 +295,21 @@ export class QueryList<T> extends AbstractComponent2<IQueryListProps<T>, IQueryL
289295
}
290296
}
291297

298+
public setActiveItem(activeItem: T | ICreateNewItem | null) {
299+
this.expectedNextActiveItem = activeItem;
300+
if (this.props.activeItem === undefined) {
301+
// indicate that the active item may need to be scrolled into view after update.
302+
this.shouldCheckActiveItemInViewport = true;
303+
this.setState({ activeItem });
304+
}
305+
306+
if (isCreateNewItem(activeItem)) {
307+
Utils.safeInvoke(this.props.onActiveItemChange, null, true);
308+
} else {
309+
Utils.safeInvoke(this.props.onActiveItemChange, activeItem, false);
310+
}
311+
}
312+
292313
/** default `itemListRenderer` implementation */
293314
private renderItemList = (listProps: IItemListRendererProps<T>) => {
294315
const { initialContent, noResults } = this.props;
@@ -486,21 +507,6 @@ export class QueryList<T> extends AbstractComponent2<IQueryListProps<T>, IQueryL
486507
return getFirstEnabledItem(this.state.filteredItems, this.props.itemDisabled, direction, startIndex);
487508
}
488509

489-
private setActiveItem(activeItem: T | ICreateNewItem | null) {
490-
this.expectedNextActiveItem = activeItem;
491-
if (this.props.activeItem === undefined) {
492-
// indicate that the active item may need to be scrolled into view after update.
493-
this.shouldCheckActiveItemInViewport = true;
494-
this.setState({ activeItem });
495-
}
496-
497-
if (isCreateNewItem(activeItem)) {
498-
Utils.safeInvoke(this.props.onActiveItemChange, null, true);
499-
} else {
500-
Utils.safeInvoke(this.props.onActiveItemChange, activeItem, false);
501-
}
502-
}
503-
504510
private isCreateItemRendered(): boolean {
505511
return (
506512
this.canCreateItems() &&

packages/select/src/components/select/suggest.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,10 +125,10 @@ export class Suggest<T> extends React.PureComponent<ISuggestProps<T>, ISuggestSt
125125
public render() {
126126
// omit props specific to this component, spread the rest.
127127
const { disabled, inputProps, popoverProps, ...restProps } = this.props;
128-
129128
return (
130129
<this.TypedQueryList
131130
{...restProps}
131+
initialActiveItem={this.props.selectedItem ?? undefined}
132132
onItemSelect={this.handleItemSelect}
133133
ref={this.refHandlers.queryList}
134134
renderer={this.renderQueryList}
@@ -142,6 +142,14 @@ export class Suggest<T> extends React.PureComponent<ISuggestProps<T>, ISuggestSt
142142
this.setState({ selectedItem: this.props.selectedItem });
143143
}
144144

145+
if (this.state.isOpen === false && prevState.isOpen === true) {
146+
// just closed, likely by keyboard interaction
147+
// wait until the transition ends so there isn't a flash of content in the popover
148+
setTimeout(() => {
149+
this.maybeResetActiveItemToSelectedItem();
150+
}, this.props.popoverProps?.transitionDuration ?? Popover.defaultProps.transitionDuration);
151+
}
152+
145153
if (this.state.isOpen && !prevState.isOpen && this.queryList != null) {
146154
this.queryList.scrollActiveItemIntoView();
147155
}
@@ -320,4 +328,13 @@ export class Suggest<T> extends React.PureComponent<ISuggestProps<T>, ISuggestSt
320328
Utils.safeInvokeMember(this.props.inputProps, "onKeyUp", evt);
321329
};
322330
};
331+
332+
private maybeResetActiveItemToSelectedItem() {
333+
const shouldResetActiveItemToSelectedItem =
334+
this.props.activeItem === undefined && this.state.selectedItem !== null && !this.props.resetOnSelect;
335+
336+
if (this.queryList !== null && shouldResetActiveItemToSelectedItem) {
337+
this.queryList.setActiveItem(this.props.selectedItem ?? this.state.selectedItem);
338+
}
339+
}
323340
}

packages/select/test/suggestTests.tsx

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import * as sinon from "sinon";
2222

2323
import { IFilm, renderFilm, TOP_100_FILMS } from "../../docs-app/src/examples/select-examples/films";
2424
import { ISuggestProps, ISuggestState, Suggest } from "../src/components/select/suggest";
25-
import { IItemRendererProps } from "../src/index";
25+
import { IItemRendererProps, QueryList } from "../src/index";
2626
import { selectComponentSuite } from "./selectComponentSuite";
2727

2828
describe("Suggest", () => {
@@ -99,6 +99,35 @@ describe("Suggest", () => {
9999
assert.strictEqual(scrollActiveItemIntoViewSpy.callCount, 1, "should call scrollActiveItemIntoView");
100100
});
101101

102+
it("sets active item to the selected item when the popover is closed", done => {
103+
// transition duration shorter than timeout below to ensure it's done
104+
const wrapper = suggest({ selectedItem: TOP_100_FILMS[10], popoverProps: { transitionDuration: 5 } });
105+
const queryList = ((wrapper.instance() as Suggest<IFilm>) as any).queryList as QueryList<IFilm>; // private ref
106+
107+
assert.deepEqual(
108+
queryList.state.activeItem,
109+
wrapper.state().selectedItem,
110+
"QueryList activeItem should be set to the controlled selectedItem if prop is provided",
111+
);
112+
113+
simulateFocus(wrapper);
114+
assert.isTrue(wrapper.state().isOpen);
115+
116+
const newActiveItem = TOP_100_FILMS[11];
117+
queryList.setActiveItem(newActiveItem);
118+
assert.deepEqual(queryList.state.activeItem, newActiveItem);
119+
120+
simulateKeyDown(wrapper, Keys.ESCAPE);
121+
assert.isFalse(wrapper.state().isOpen);
122+
123+
wrapper.update();
124+
wrapper.find(QueryList).update();
125+
setTimeout(() => {
126+
assert.deepEqual(queryList.state.activeItem, wrapper.state().selectedItem);
127+
done();
128+
}, 10);
129+
});
130+
102131
function checkKeyDownDoesNotOpenPopover(wrapper: ReactWrapper<any, any>, which: number) {
103132
simulateKeyDown(wrapper, which);
104133
assert.isFalse(wrapper.state().isOpen, "should not open popover");

0 commit comments

Comments
 (0)