UNPKG

14.9 kBJavaScriptView Raw
1/*
2 * Copyright 2022 Palantir Technologies, Inc. All rights reserved.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16import { __assign, __extends, __rest } from "tslib";
17import classNames from "classnames";
18import * as React from "react";
19import { AbstractPureComponent, DISPLAYNAME_PREFIX, InputGroup, mergeRefs, Popover, PopupKind, refHandler, setRef, Utils, } from "@blueprintjs/core";
20import { Classes } from "../../common";
21import { QueryList } from "../query-list/queryList";
22/**
23 * Suggest component.
24 *
25 * @see https://blueprintjs.com/docs/#select/suggest
26 */
27var Suggest = /** @class */ (function (_super) {
28 __extends(Suggest, _super);
29 function Suggest() {
30 var _a;
31 var _this = _super.apply(this, arguments) || this;
32 _this.state = {
33 isOpen: (_this.props.popoverProps != null && _this.props.popoverProps.isOpen) || false,
34 selectedItem: _this.getInitialSelectedItem(),
35 };
36 _this.inputElement = null;
37 _this.queryList = null;
38 _this.handleInputRef = refHandler(_this, "inputElement", (_a = _this.props.inputProps) === null || _a === void 0 ? void 0 : _a.inputRef);
39 _this.handleQueryListRef = function (ref) { return (_this.queryList = ref); };
40 _this.listboxId = Utils.uniqueId("listbox");
41 _this.renderQueryList = function (listProps) {
42 var _a = _this.props, _b = _a.popoverContentProps, popoverContentProps = _b === void 0 ? {} : _b, _c = _a.popoverProps, popoverProps = _c === void 0 ? {} : _c, popoverRef = _a.popoverRef;
43 var isOpen = _this.state.isOpen;
44 var handleKeyDown = listProps.handleKeyDown, handleKeyUp = listProps.handleKeyUp;
45 // N.B. no need to set `popoverProps.fill` since that is unused with the `renderTarget` API
46 return (React.createElement(Popover, __assign({ autoFocus: false, enforceFocus: false, isOpen: isOpen, placement: popoverProps.position || popoverProps.placement ? undefined : "bottom-start" }, popoverProps, { className: classNames(listProps.className, popoverProps.className), content: React.createElement("div", __assign({}, popoverContentProps, { onKeyDown: handleKeyDown, onKeyUp: handleKeyUp }), listProps.itemList), interactionKind: "click", onInteraction: _this.handlePopoverInteraction, onOpened: _this.handlePopoverOpened, onOpening: _this.handlePopoverOpening, popoverClassName: classNames(Classes.SUGGEST_POPOVER, popoverProps.popoverClassName), popupKind: PopupKind.LISTBOX, ref: popoverRef, renderTarget: _this.getPopoverTargetRenderer(listProps, isOpen) })));
47 };
48 // We use the renderTarget API to flatten the rendered DOM and make it easier to implement features like
49 // the "fill" prop. Note that we must take `isOpen` as an argument to force this render function to be called
50 // again after that state changes.
51 _this.getPopoverTargetRenderer = function (listProps, isOpen) {
52 // eslint-disable-next-line react/display-name
53 return function (_a) {
54 var
55 // pull out `isOpen` so that it's not forwarded to the DOM
56 _isOpen = _a.isOpen, ref = _a.ref, targetProps = __rest(_a, ["isOpen", "ref"]);
57 var _b = _this.props, disabled = _b.disabled, fill = _b.fill, _c = _b.inputProps, inputProps = _c === void 0 ? {} : _c, inputValueRenderer = _b.inputValueRenderer, _d = _b.popoverProps, popoverProps = _d === void 0 ? {} : _d, resetOnClose = _b.resetOnClose;
58 var selectedItem = _this.state.selectedItem;
59 var handleKeyDown = listProps.handleKeyDown, handleKeyUp = listProps.handleKeyUp;
60 var selectedItemText = selectedItem == null ? "" : inputValueRenderer(selectedItem);
61 var _e = inputProps.autoComplete, autoComplete = _e === void 0 ? "off" : _e, _f = inputProps.placeholder, placeholder = _f === void 0 ? "Search..." : _f;
62 // placeholder shows selected item while open.
63 var inputPlaceholder = isOpen && selectedItemText ? selectedItemText : placeholder;
64 // value shows query when open, and query remains when closed if nothing is selected.
65 // if resetOnClose is enabled, then hide query when not open. (see handlePopoverOpening)
66 var inputValue = isOpen
67 ? listProps.query
68 : selectedItemText === ""
69 ? resetOnClose
70 ? ""
71 : listProps.query
72 : selectedItemText;
73 return (React.createElement(InputGroup, __assign({ "aria-controls": _this.listboxId, autoComplete: autoComplete, disabled: disabled, tagName: popoverProps.targetTagName }, targetProps, inputProps, { "aria-autocomplete": "list", "aria-expanded": isOpen, className: classNames(targetProps.className, inputProps.className), fill: fill, inputRef: mergeRefs(_this.handleInputRef, ref), onChange: listProps.handleQueryChange, onFocus: _this.handleInputFocus, onKeyDown: _this.getTargetKeyDownHandler(handleKeyDown), onKeyUp: _this.getTargetKeyUpHandler(handleKeyUp), placeholder: inputPlaceholder, role: "combobox", value: inputValue })));
74 };
75 };
76 _this.selectText = function () {
77 // wait until the input is properly focused to select the text inside of it
78 _this.requestAnimationFrame(function () {
79 var _a;
80 (_a = _this.inputElement) === null || _a === void 0 ? void 0 : _a.setSelectionRange(0, _this.inputElement.value.length);
81 });
82 };
83 _this.handleInputFocus = function (event) {
84 var _a, _b;
85 _this.selectText();
86 // TODO can we leverage Popover.openOnTargetFocus for this?
87 if (!_this.props.openOnKeyDown) {
88 _this.setState({ isOpen: true });
89 }
90 (_b = (_a = _this.props.inputProps) === null || _a === void 0 ? void 0 : _a.onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, event);
91 };
92 _this.handleItemSelect = function (item, event) {
93 var _a, _b, _c, _d;
94 var nextOpenState;
95 if (!_this.props.closeOnSelect) {
96 (_a = _this.inputElement) === null || _a === void 0 ? void 0 : _a.focus();
97 _this.selectText();
98 nextOpenState = true;
99 }
100 else {
101 (_b = _this.inputElement) === null || _b === void 0 ? void 0 : _b.blur();
102 nextOpenState = false;
103 }
104 // the internal state should only change when uncontrolled.
105 if (_this.props.selectedItem === undefined) {
106 _this.setState({
107 isOpen: nextOpenState,
108 selectedItem: item,
109 });
110 }
111 else {
112 // otherwise just set the next open state.
113 _this.setState({ isOpen: nextOpenState });
114 }
115 (_d = (_c = _this.props).onItemSelect) === null || _d === void 0 ? void 0 : _d.call(_c, item, event);
116 };
117 // Popover interaction kind is CLICK, so this only handles click events.
118 // Note that we defer to the next animation frame in order to get the latest activeElement
119 _this.handlePopoverInteraction = function (nextOpenState, event) {
120 return _this.requestAnimationFrame(function () {
121 var _a, _b;
122 var isInputFocused = _this.inputElement === Utils.getActiveElement(_this.inputElement);
123 if (_this.inputElement != null && !isInputFocused) {
124 // the input is no longer focused, we should close the popover
125 _this.setState({ isOpen: false });
126 }
127 (_b = (_a = _this.props.popoverProps) === null || _a === void 0 ? void 0 : _a.onInteraction) === null || _b === void 0 ? void 0 : _b.call(_a, nextOpenState, event);
128 });
129 };
130 _this.handlePopoverOpening = function (node) {
131 var _a, _b;
132 // reset query before opening instead of when closing to prevent flash of unfiltered items.
133 // this is a limitation of the interactions between QueryList state and Popover transitions.
134 if (_this.props.resetOnClose && _this.queryList) {
135 _this.queryList.setQuery("", true);
136 }
137 (_b = (_a = _this.props.popoverProps) === null || _a === void 0 ? void 0 : _a.onOpening) === null || _b === void 0 ? void 0 : _b.call(_a, node);
138 };
139 _this.handlePopoverOpened = function (node) {
140 var _a, _b;
141 // scroll active item into view after popover transition completes and all dimensions are stable.
142 if (_this.queryList != null) {
143 _this.queryList.scrollActiveItemIntoView();
144 }
145 (_b = (_a = _this.props.popoverProps) === null || _a === void 0 ? void 0 : _a.onOpened) === null || _b === void 0 ? void 0 : _b.call(_a, node);
146 };
147 _this.getTargetKeyDownHandler = function (handleQueryListKeyDown) {
148 return function (e) {
149 var _a, _b, _c;
150 if (e.key === "Escape" || e.key === "Tab") {
151 (_a = _this.inputElement) === null || _a === void 0 ? void 0 : _a.blur();
152 _this.setState({ isOpen: false });
153 }
154 else if (_this.props.openOnKeyDown &&
155 e.key !== "Backspace" &&
156 e.key !== "ArrowLeft" &&
157 e.key !== "ArrowRight") {
158 _this.setState({ isOpen: true });
159 }
160 if (_this.state.isOpen) {
161 handleQueryListKeyDown === null || handleQueryListKeyDown === void 0 ? void 0 : handleQueryListKeyDown(e);
162 }
163 (_c = (_b = _this.props.inputProps) === null || _b === void 0 ? void 0 : _b.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(_b, e);
164 };
165 };
166 _this.getTargetKeyUpHandler = function (handleQueryListKeyUp) {
167 return function (evt) {
168 var _a, _b;
169 if (_this.state.isOpen) {
170 handleQueryListKeyUp === null || handleQueryListKeyUp === void 0 ? void 0 : handleQueryListKeyUp(evt);
171 }
172 (_b = (_a = _this.props.inputProps) === null || _a === void 0 ? void 0 : _a.onKeyUp) === null || _b === void 0 ? void 0 : _b.call(_a, evt);
173 };
174 };
175 return _this;
176 }
177 /** @deprecated no longer necessary now that the TypeScript parser supports type arguments on JSX element tags */
178 Suggest.ofType = function () {
179 return Suggest;
180 };
181 Suggest.prototype.render = function () {
182 var _a;
183 // omit props specific to this component, spread the rest.
184 var _b = this.props, disabled = _b.disabled, inputProps = _b.inputProps, menuProps = _b.menuProps, popoverProps = _b.popoverProps, restProps = __rest(_b, ["disabled", "inputProps", "menuProps", "popoverProps"]);
185 return (React.createElement(QueryList, __assign({}, restProps, { menuProps: __assign(__assign({ "aria-label": "selectable options" }, menuProps), { id: this.listboxId }), initialActiveItem: (_a = this.props.selectedItem) !== null && _a !== void 0 ? _a : undefined, onItemSelect: this.handleItemSelect, ref: this.handleQueryListRef, renderer: this.renderQueryList })));
186 };
187 Suggest.prototype.componentDidUpdate = function (prevProps, prevState) {
188 var _this = this;
189 var _a, _b, _c, _d, _e, _f, _g;
190 if (((_a = prevProps.inputProps) === null || _a === void 0 ? void 0 : _a.inputRef) !== ((_b = this.props.inputProps) === null || _b === void 0 ? void 0 : _b.inputRef)) {
191 setRef((_c = prevProps.inputProps) === null || _c === void 0 ? void 0 : _c.inputRef, null);
192 this.handleInputRef = refHandler(this, "inputElement", (_d = this.props.inputProps) === null || _d === void 0 ? void 0 : _d.inputRef);
193 setRef((_e = this.props.inputProps) === null || _e === void 0 ? void 0 : _e.inputRef, this.inputElement);
194 }
195 // If the selected item prop changes, update the underlying state.
196 if (this.props.selectedItem !== undefined && this.props.selectedItem !== this.state.selectedItem) {
197 this.setState({ selectedItem: this.props.selectedItem });
198 }
199 if (this.state.isOpen === false && prevState.isOpen === true) {
200 // just closed, likely by keyboard interaction
201 // wait until the transition ends so there isn't a flash of content in the popover
202 var timeout = (_g = (_f = this.props.popoverProps) === null || _f === void 0 ? void 0 : _f.transitionDuration) !== null && _g !== void 0 ? _g : Popover.defaultProps.transitionDuration;
203 setTimeout(function () { return _this.maybeResetActiveItemToSelectedItem(); }, timeout);
204 }
205 if (this.state.isOpen && !prevState.isOpen && this.queryList != null) {
206 this.queryList.scrollActiveItemIntoView();
207 }
208 };
209 Suggest.prototype.getInitialSelectedItem = function () {
210 // controlled > uncontrolled > default
211 if (this.props.selectedItem !== undefined) {
212 return this.props.selectedItem;
213 }
214 else if (this.props.defaultSelectedItem !== undefined) {
215 return this.props.defaultSelectedItem;
216 }
217 else {
218 return null;
219 }
220 };
221 Suggest.prototype.maybeResetActiveItemToSelectedItem = function () {
222 var _a;
223 var shouldResetActiveItemToSelectedItem = this.props.activeItem === undefined && this.state.selectedItem !== null && !this.props.resetOnSelect;
224 if (this.queryList !== null && shouldResetActiveItemToSelectedItem) {
225 this.queryList.setActiveItem((_a = this.props.selectedItem) !== null && _a !== void 0 ? _a : this.state.selectedItem);
226 }
227 };
228 Suggest.displayName = "".concat(DISPLAYNAME_PREFIX, ".Suggest");
229 Suggest.defaultProps = {
230 closeOnSelect: true,
231 fill: false,
232 openOnKeyDown: false,
233 resetOnClose: false,
234 };
235 return Suggest;
236}(AbstractPureComponent));
237export { Suggest };
238//# sourceMappingURL=suggest.js.map
\No newline at end of file