UNPKG

15.4 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";
17/** @fileoverview "V2" variant of Suggest which uses Popover2 instead of Popover2 */
18import classNames from "classnames";
19import * as React from "react";
20import { AbstractPureComponent2, DISPLAYNAME_PREFIX, InputGroup, Keys, mergeRefs, refHandler, setRef, Utils, } from "@blueprintjs/core";
21import { Popover2, PopupKind } from "@blueprintjs/popover2";
22import { Classes } from "../../common";
23import { QueryList } from "../query-list/queryList";
24/**
25 * Suggest (v2) component.
26 *
27 * @see https://blueprintjs.com/docs/#select/suggest2
28 */
29var Suggest2 = /** @class */ (function (_super) {
30 __extends(Suggest2, _super);
31 function Suggest2() {
32 var _this = this;
33 var _a;
34 _this = _super.apply(this, arguments) || this;
35 _this.state = {
36 isOpen: (_this.props.popoverProps != null && _this.props.popoverProps.isOpen) || false,
37 selectedItem: _this.getInitialSelectedItem(),
38 };
39 _this.inputElement = null;
40 _this.queryList = null;
41 _this.handleInputRef = refHandler(_this, "inputElement", (_a = _this.props.inputProps) === null || _a === void 0 ? void 0 : _a.inputRef);
42 _this.handleQueryListRef = function (ref) { return (_this.queryList = ref); };
43 _this.listboxId = Utils.uniqueId("listbox");
44 _this.renderQueryList = function (listProps) {
45 var _a = _this.props, _b = _a.popoverContentProps, popoverContentProps = _b === void 0 ? {} : _b, _c = _a.popoverProps, popoverProps = _c === void 0 ? {} : _c, popoverRef = _a.popoverRef;
46 var isOpen = _this.state.isOpen;
47 var handleKeyDown = listProps.handleKeyDown, handleKeyUp = listProps.handleKeyUp;
48 // N.B. no need to set `popoverProps.fill` since that is unused with the `renderTarget` API
49 return (React.createElement(Popover2, __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) })));
50 };
51 // We use the renderTarget API to flatten the rendered DOM and make it easier to implement features like
52 // the "fill" prop. Note that we must take `isOpen` as an argument to force this render function to be called
53 // again after that state changes.
54 _this.getPopoverTargetRenderer = function (listProps, isOpen) {
55 // eslint-disable-next-line react/display-name
56 return function (_a) {
57 var
58 // pull out `isOpen` so that it's not forwarded to the DOM
59 _isOpen = _a.isOpen,
60 // pull out `defaultValue` due to type incompatibility with InputGroup
61 defaultValue = _a.defaultValue, ref = _a.ref, targetProps = __rest(_a, ["isOpen", "defaultValue", "ref"]);
62 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;
63 var selectedItem = _this.state.selectedItem;
64 var handleKeyDown = listProps.handleKeyDown, handleKeyUp = listProps.handleKeyUp;
65 var selectedItemText = selectedItem == null ? "" : inputValueRenderer(selectedItem);
66 var _e = inputProps.autoComplete, autoComplete = _e === void 0 ? "off" : _e, _f = inputProps.placeholder, placeholder = _f === void 0 ? "Search..." : _f;
67 // placeholder shows selected item while open.
68 var inputPlaceholder = isOpen && selectedItemText ? selectedItemText : placeholder;
69 // value shows query when open, and query remains when closed if nothing is selected.
70 // if resetOnClose is enabled, then hide query when not open. (see handlePopoverOpening)
71 var inputValue = isOpen ? listProps.query : selectedItemText !== null && selectedItemText !== void 0 ? selectedItemText : (resetOnClose ? "" : listProps.query);
72 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 })));
73 };
74 };
75 _this.selectText = function () {
76 // wait until the input is properly focused to select the text inside of it
77 _this.requestAnimationFrame(function () {
78 var _a;
79 (_a = _this.inputElement) === null || _a === void 0 ? void 0 : _a.setSelectionRange(0, _this.inputElement.value.length);
80 });
81 };
82 _this.handleInputFocus = function (event) {
83 var _a, _b;
84 _this.selectText();
85 // TODO can we leverage Popover2.openOnTargetFocus for this?
86 if (!_this.props.openOnKeyDown) {
87 _this.setState({ isOpen: true });
88 }
89 (_b = (_a = _this.props.inputProps) === null || _a === void 0 ? void 0 : _a.onFocus) === null || _b === void 0 ? void 0 : _b.call(_a, event);
90 };
91 _this.handleItemSelect = function (item, event) {
92 var _a, _b, _c, _d;
93 var nextOpenState;
94 if (!_this.props.closeOnSelect) {
95 (_a = _this.inputElement) === null || _a === void 0 ? void 0 : _a.focus();
96 _this.selectText();
97 nextOpenState = true;
98 }
99 else {
100 (_b = _this.inputElement) === null || _b === void 0 ? void 0 : _b.blur();
101 nextOpenState = false;
102 }
103 // the internal state should only change when uncontrolled.
104 if (_this.props.selectedItem === undefined) {
105 _this.setState({
106 isOpen: nextOpenState,
107 selectedItem: item,
108 });
109 }
110 else {
111 // otherwise just set the next open state.
112 _this.setState({ isOpen: nextOpenState });
113 }
114 (_d = (_c = _this.props).onItemSelect) === null || _d === void 0 ? void 0 : _d.call(_c, item, event);
115 };
116 // Popover2 interaction kind is CLICK, so this only handles click events.
117 // Note that we defer to the next animation frame in order to get the latest activeElement
118 _this.handlePopoverInteraction = function (nextOpenState, event) {
119 return _this.requestAnimationFrame(function () {
120 var _a, _b;
121 var isInputFocused = _this.inputElement === Utils.getActiveElement(_this.inputElement);
122 if (_this.inputElement != null && !isInputFocused) {
123 // the input is no longer focused, we should close the popover
124 _this.setState({ isOpen: false });
125 }
126 (_b = (_a = _this.props.popoverProps) === null || _a === void 0 ? void 0 : _a.onInteraction) === null || _b === void 0 ? void 0 : _b.call(_a, nextOpenState, event);
127 });
128 };
129 _this.handlePopoverOpening = function (node) {
130 var _a, _b;
131 // reset query before opening instead of when closing to prevent flash of unfiltered items.
132 // this is a limitation of the interactions between QueryList state and Popover2 transitions.
133 if (_this.props.resetOnClose && _this.queryList) {
134 _this.queryList.setQuery("", true);
135 }
136 (_b = (_a = _this.props.popoverProps) === null || _a === void 0 ? void 0 : _a.onOpening) === null || _b === void 0 ? void 0 : _b.call(_a, node);
137 };
138 _this.handlePopoverOpened = function (node) {
139 var _a, _b;
140 // scroll active item into view after popover transition completes and all dimensions are stable.
141 if (_this.queryList != null) {
142 _this.queryList.scrollActiveItemIntoView();
143 }
144 (_b = (_a = _this.props.popoverProps) === null || _a === void 0 ? void 0 : _a.onOpened) === null || _b === void 0 ? void 0 : _b.call(_a, node);
145 };
146 _this.getTargetKeyDownHandler = function (handleQueryListKeyDown) {
147 return function (evt) {
148 var _a, _b, _c;
149 // HACKHACK: https://github.com/palantir/blueprint/issues/4165
150 // eslint-disable-next-line deprecation/deprecation
151 var which = evt.which;
152 if (which === Keys.ESCAPE || which === Keys.TAB) {
153 (_a = _this.inputElement) === null || _a === void 0 ? void 0 : _a.blur();
154 _this.setState({ isOpen: false });
155 }
156 else if (_this.props.openOnKeyDown &&
157 which !== Keys.BACKSPACE &&
158 which !== Keys.ARROW_LEFT &&
159 which !== Keys.ARROW_RIGHT) {
160 _this.setState({ isOpen: true });
161 }
162 if (_this.state.isOpen) {
163 handleQueryListKeyDown === null || handleQueryListKeyDown === void 0 ? void 0 : handleQueryListKeyDown(evt);
164 }
165 (_c = (_b = _this.props.inputProps) === null || _b === void 0 ? void 0 : _b.onKeyDown) === null || _c === void 0 ? void 0 : _c.call(_b, evt);
166 };
167 };
168 _this.getTargetKeyUpHandler = function (handleQueryListKeyUp) {
169 return function (evt) {
170 var _a, _b;
171 if (_this.state.isOpen) {
172 handleQueryListKeyUp === null || handleQueryListKeyUp === void 0 ? void 0 : handleQueryListKeyUp(evt);
173 }
174 (_b = (_a = _this.props.inputProps) === null || _a === void 0 ? void 0 : _a.onKeyUp) === null || _b === void 0 ? void 0 : _b.call(_a, evt);
175 };
176 };
177 return _this;
178 }
179 /** @deprecated no longer necessary now that the TypeScript parser supports type arguments on JSX element tags */
180 Suggest2.ofType = function () {
181 return Suggest2;
182 };
183 Suggest2.prototype.render = function () {
184 var _a;
185 // omit props specific to this component, spread the rest.
186 var _b = this.props, disabled = _b.disabled, inputProps = _b.inputProps, menuProps = _b.menuProps, popoverProps = _b.popoverProps, restProps = __rest(_b, ["disabled", "inputProps", "menuProps", "popoverProps"]);
187 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 })));
188 };
189 Suggest2.prototype.componentDidUpdate = function (prevProps, prevState) {
190 var _this = this;
191 var _a, _b, _c, _d, _e, _f, _g;
192 if (((_a = prevProps.inputProps) === null || _a === void 0 ? void 0 : _a.inputRef) !== ((_b = this.props.inputProps) === null || _b === void 0 ? void 0 : _b.inputRef)) {
193 setRef((_c = prevProps.inputProps) === null || _c === void 0 ? void 0 : _c.inputRef, null);
194 this.handleInputRef = refHandler(this, "inputElement", (_d = this.props.inputProps) === null || _d === void 0 ? void 0 : _d.inputRef);
195 setRef((_e = this.props.inputProps) === null || _e === void 0 ? void 0 : _e.inputRef, this.inputElement);
196 }
197 // If the selected item prop changes, update the underlying state.
198 if (this.props.selectedItem !== undefined && this.props.selectedItem !== this.state.selectedItem) {
199 this.setState({ selectedItem: this.props.selectedItem });
200 }
201 if (this.state.isOpen === false && prevState.isOpen === true) {
202 // just closed, likely by keyboard interaction
203 // wait until the transition ends so there isn't a flash of content in the popover
204 /* eslint-disable-next-line deprecation/deprecation */
205 var timeout = (_g = (_f = this.props.popoverProps) === null || _f === void 0 ? void 0 : _f.transitionDuration) !== null && _g !== void 0 ? _g : Popover2.defaultProps.transitionDuration;
206 setTimeout(function () { return _this.maybeResetActiveItemToSelectedItem(); }, timeout);
207 }
208 if (this.state.isOpen && !prevState.isOpen && this.queryList != null) {
209 this.queryList.scrollActiveItemIntoView();
210 }
211 };
212 Suggest2.prototype.getInitialSelectedItem = function () {
213 // controlled > uncontrolled > default
214 if (this.props.selectedItem !== undefined) {
215 return this.props.selectedItem;
216 }
217 else if (this.props.defaultSelectedItem !== undefined) {
218 return this.props.defaultSelectedItem;
219 }
220 else {
221 return null;
222 }
223 };
224 Suggest2.prototype.maybeResetActiveItemToSelectedItem = function () {
225 var _a;
226 var shouldResetActiveItemToSelectedItem = this.props.activeItem === undefined && this.state.selectedItem !== null && !this.props.resetOnSelect;
227 if (this.queryList !== null && shouldResetActiveItemToSelectedItem) {
228 this.queryList.setActiveItem((_a = this.props.selectedItem) !== null && _a !== void 0 ? _a : this.state.selectedItem);
229 }
230 };
231 Suggest2.displayName = "".concat(DISPLAYNAME_PREFIX, ".Suggest2");
232 Suggest2.defaultProps = {
233 closeOnSelect: true,
234 fill: false,
235 openOnKeyDown: false,
236 resetOnClose: false,
237 };
238 return Suggest2;
239}(AbstractPureComponent2));
240export { Suggest2 };
241//# sourceMappingURL=suggest2.js.map
\No newline at end of file