Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Suggest] Added selectedItem prop #2874

Merged
merged 9 commits into from
Sep 5, 2018
32 changes: 26 additions & 6 deletions packages/select/src/components/select/suggest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ export interface ISuggestProps<T> extends IListItemsProps<T> {
/** Custom renderer to transform an item into a string for the input value. */
inputValueRenderer: (item: T) => string;

/**
* The currently selected item, or `null` to indicate that no item is selected.
* If omitted, this prop will be uncontrolled (managed by the component's state).
* Use `onItemSelect` to listen for updates.
*/
selectedItem?: T | null;

/**
* Whether the popover opens on key down or when the input is focused.
* @default false
Expand All @@ -50,7 +57,7 @@ export interface ISuggestProps<T> extends IListItemsProps<T> {

export interface ISuggestState<T> {
isOpen: boolean;
selectedItem?: T;
selectedItem: T | null;
}

export class Suggest<T> extends React.PureComponent<ISuggestProps<T>, ISuggestState<T>> {
Expand All @@ -68,6 +75,7 @@ export class Suggest<T> extends React.PureComponent<ISuggestProps<T>, ISuggestSt

public state: ISuggestState<T> = {
isOpen: (this.props.popoverProps && this.props.popoverProps.isOpen) || false,
selectedItem: this.props.selectedItem !== undefined ? this.props.selectedItem : null,
};

private TypedQueryList = QueryList.ofType<T>();
Expand Down Expand Up @@ -96,6 +104,13 @@ export class Suggest<T> extends React.PureComponent<ISuggestProps<T>, ISuggestSt
);
}

public componentWillReceiveProps(nextProps: ISuggestProps<T>) {
// If the selected item prop changes, update the underlying state.
if (nextProps.selectedItem !== undefined && nextProps.selectedItem !== this.state.selectedItem) {
this.setState({ selectedItem: nextProps.selectedItem });
}
}

public componentDidUpdate(_prevProps: ISuggestProps<T>, prevState: ISuggestState<T>) {
if (this.state.isOpen && !prevState.isOpen && this.queryList != null) {
this.queryList.scrollActiveItemIntoView();
Expand Down Expand Up @@ -174,11 +189,16 @@ export class Suggest<T> extends React.PureComponent<ISuggestProps<T>, ISuggestSt
}
nextOpenState = false;
}

this.setState({
isOpen: nextOpenState,
selectedItem: item,
});
// the internal state should only change when uncontrolled.
if (this.props.selectedItem === undefined) {
this.setState({
isOpen: nextOpenState,
selectedItem: item,
});
} else {
// otherwise just set the next open state.
this.setState({ isOpen: nextOpenState });
}

Utils.safeInvoke(this.props.onItemSelect, item, event);
};
Expand Down
42 changes: 42 additions & 0 deletions packages/select/test/suggestTests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,48 @@ describe("Suggest", () => {
}
});

describe("Controlled Mode", () => {
danilotorrisi marked this conversation as resolved.
Show resolved Hide resolved
it("initialize the selectedItem with the given value", () => {
const selectedItem = TOP_100_FILMS[0];
assert.isNotNull(selectedItem, "The selected item we test must not be null");
const wrapper = suggest({ selectedItem });
assert.strictEqual(wrapper.state().selectedItem, selectedItem);
});
it("propagates the selectedItem with new values", () => {
const selectedItem = TOP_100_FILMS[0];
assert.isNotNull(selectedItem, "The selected item we test must not be null");
const wrapper = suggest();
assert.isNull(wrapper.state().selectedItem);
wrapper.setProps({ selectedItem });
assert.strictEqual(wrapper.state().selectedItem, selectedItem);
});
it("when new item selected, it should respect the selectedItem prop", () => {
const selectedItem = TOP_100_FILMS[0];
const ITEM_INDEX = 4;
assert.isNotNull(selectedItem, "The selected item we test must not be null");
const wrapper = suggest({ selectedItem });
simulateFocus(wrapper);
selectItem(wrapper, ITEM_INDEX);
assert.isTrue(handlers.onItemSelect.called, "onItemSelect should be called after selection");
assert.strictEqual(wrapper.state().selectedItem, selectedItem, "the underlying state should not change");
const newSelectedItem = TOP_100_FILMS[ITEM_INDEX];
wrapper.setProps({ selectedItem: newSelectedItem });
assert.strictEqual(wrapper.state().selectedItem, newSelectedItem, "the selectedItem should be updated");
});
it("preserves the empty selection", () => {
const ITEM_INDEX = 4;
const selectedItem = TOP_100_FILMS[0];
const wrapper = suggest({ selectedItem: null });
assert.isNull(wrapper.state().selectedItem);
simulateFocus(wrapper);
selectItem(wrapper, ITEM_INDEX);
assert.isTrue(handlers.onItemSelect.called, "onItemSelect should be called after selection");
assert.isNull(wrapper.state().selectedItem, "the underlying state should not change");
wrapper.setProps({ selectedItem });
assert.strictEqual(wrapper.state().selectedItem, selectedItem, "the selectedItem should be updated");
});
});

function suggest(props: Partial<ISuggestProps<IFilm>> = {}, query?: string) {
const wrapper = mount(<FilmSuggest {...defaultProps} {...handlers} {...props} />);
if (query !== undefined) {
Expand Down