calcite-combobox.js 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915
  1. /*!
  2. * All material copyright ESRI, All Rights Reserved, unless otherwise specified.
  3. * See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
  4. * v1.0.0-beta.97
  5. */
  6. import { proxyCustomElement, HTMLElement, createEvent, h, Host } from '@stencil/core/internal/client/index.js';
  7. import { f as filter } from './filter2.js';
  8. import { d as defaultMenuPlacement, f as filterComputedPlacements, c as connectFloatingUI, u as updateAfterClose, r as reposition, a as disconnectFloatingUI, F as FloatingCSS } from './floating-ui.js';
  9. import { g as guid } from './guid.js';
  10. import { T as TEXT, C as ComboboxItemGroup, a as ComboboxChildSelector, g as getItemAncestors, b as getItemChildren, h as hasActiveChildren, c as ComboboxItem } from './utils2.js';
  11. import { c as connectLabel, d as disconnectLabel, g as getLabelText } from './label2.js';
  12. import { s as submitForm, c as connectForm, a as afterConnectDefaultValueSet, d as disconnectForm, H as HiddenFormInputSlot } from './form.js';
  13. import { c as createObserver } from './observers.js';
  14. import { u as updateHostInteraction } from './interactive.js';
  15. import { i as isPrimaryPointerButton, t as toAriaBoolean } from './dom.js';
  16. import { c as connectOpenCloseComponent, d as disconnectOpenCloseComponent } from './openCloseComponent.js';
  17. import { d as defineCustomElement$3 } from './chip.js';
  18. import { d as defineCustomElement$2 } from './icon.js';
  19. import { d as debounce } from './debounce.js';
  20. const comboboxCss = "@keyframes in{0%{opacity:0}100%{opacity:1}}@keyframes in-down{0%{opacity:0;transform:translate3D(0, -5px, 0)}100%{opacity:1;transform:translate3D(0, 0, 0)}}@keyframes in-up{0%{opacity:0;transform:translate3D(0, 5px, 0)}100%{opacity:1;transform:translate3D(0, 0, 0)}}@keyframes in-scale{0%{opacity:0;transform:scale3D(0.95, 0.95, 1)}100%{opacity:1;transform:scale3D(1, 1, 1)}}:root{--calcite-animation-timing:calc(150ms * var(--calcite-internal-duration-factor));--calcite-internal-duration-factor:var(--calcite-duration-factor, 1);--calcite-internal-animation-timing-fast:calc(100ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-medium:calc(200ms * var(--calcite-internal-duration-factor));--calcite-internal-animation-timing-slow:calc(300ms * var(--calcite-internal-duration-factor))}.calcite-animate{opacity:0;animation-fill-mode:both;animation-duration:var(--calcite-animation-timing)}.calcite-animate__in{animation-name:in}.calcite-animate__in-down{animation-name:in-down}.calcite-animate__in-up{animation-name:in-up}.calcite-animate__in-scale{animation-name:in-scale}@media (prefers-reduced-motion: reduce){:root{--calcite-internal-duration-factor:0.01}}:root{--calcite-floating-ui-transition:var(--calcite-animation-timing)}:host([hidden]){display:none}:host([disabled]){pointer-events:none;cursor:default;-webkit-user-select:none;user-select:none;opacity:var(--calcite-ui-opacity-disabled)}:host{position:relative;display:block}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}:host([scale=s]){font-size:var(--calcite-font-size--2);--calcite-combobox-item-spacing-unit-l:0.5rem;--calcite-combobox-item-spacing-unit-s:0.25rem;--calcite-combobox-input-height:1.5rem}:host([scale=m]){font-size:var(--calcite-font-size--1);--calcite-combobox-item-spacing-unit-l:0.75rem;--calcite-combobox-item-spacing-unit-s:0.5rem;--calcite-combobox-input-height:2rem}:host([scale=l]){font-size:var(--calcite-font-size-0);--calcite-combobox-item-spacing-unit-l:1rem;--calcite-combobox-item-spacing-unit-s:0.75rem;--calcite-combobox-input-height:2.75rem}.wrapper{display:flex;border-width:1px;border-style:solid;border-color:var(--calcite-ui-border-input);background-color:var(--calcite-ui-foreground-1);color:var(--calcite-ui-text-1);outline-color:transparent;padding-block:calc(var(--calcite-combobox-item-spacing-unit-s) / 4);padding-inline:var(--calcite-combobox-item-spacing-unit-l)}:host(:focus-within) .wrapper,.wrapper--active{outline:2px solid var(--calcite-ui-brand);outline-offset:-2px}.wrapper--single{padding-block:0;padding-inline:var(--calcite-combobox-item-spacing-unit-l);cursor:pointer;flex-wrap:nowrap}.grid-input{display:flex;flex-grow:1;flex-wrap:wrap;align-items:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0px}.input{flex-grow:1;-webkit-appearance:none;appearance:none;border-style:none;background-color:transparent;padding:0px;font-family:inherit;color:var(--calcite-ui-text-1);font-size:inherit;block-size:var(--calcite-combobox-input-height);line-height:var(--calcite-combobox-input-height);min-inline-size:120px;margin-block-end:var(--calcite-combobox-item-spacing-unit-s)}.input:focus{outline:2px solid transparent;outline-offset:2px}.input--transparent{opacity:0}.input--single{margin-block:0px;padding:0px}.wrapper--active .input-single{cursor:text}.input--hidden{pointer-events:none;inline-size:0px;min-inline-size:0px;opacity:0}.input--icon{padding-block:0;padding-inline:var(--calcite-combobox-item-spacing-unit-l)}.input-wrap{display:flex;flex-grow:1}.input-wrap--single{flex:1 1 0%;overflow:hidden}.label{pointer-events:none;display:flex;max-inline-size:100%;flex:1 1 auto;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0px;font-weight:var(--calcite-font-weight-normal);block-size:var(--calcite-combobox-input-height);line-height:var(--calcite-combobox-input-height)}.label--icon{padding-inline:var(--calcite-combobox-item-spacing-unit-l)}.icon-end,.icon-start{display:flex;inline-size:1rem;cursor:pointer;align-items:center}.icon-end{flex:none}.floating-ui-container{display:block;position:absolute;z-index:900;visibility:hidden;pointer-events:none;inline-size:0;block-size:0;overflow:hidden}.floating-ui-container .calcite-floating-ui-anim{position:relative;transition:var(--calcite-floating-ui-transition);visibility:hidden;transition-property:transform, visibility, opacity;opacity:0;box-shadow:0 0 16px 0 rgba(0, 0, 0, 0.16);z-index:1;border-radius:0.25rem}.floating-ui-container[data-placement^=bottom] .calcite-floating-ui-anim{transform:translateY(-5px)}.floating-ui-container[data-placement^=top] .calcite-floating-ui-anim{transform:translateY(5px)}.floating-ui-container[data-placement^=left] .calcite-floating-ui-anim{transform:translateX(5px)}.floating-ui-container[data-placement^=right] .calcite-floating-ui-anim{transform:translateX(-5px)}.floating-ui-container[data-placement] .calcite-floating-ui-anim--active{opacity:1;visibility:visible;transform:translate(0)}.floating-ui-container--active{pointer-events:initial;visibility:visible;inline-size:unset;block-size:unset;overflow:unset}@media (forced-colors: active){.wrapper,.floating-ui-container--active{border:1px solid canvasText}}.screen-readers-only{position:absolute;inline-size:1px;block-size:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0}.list-container{max-block-size:45vh;overflow-y:auto;background-color:var(--calcite-ui-foreground-1);inline-size:var(--calcite-dropdown-width)}.list{margin:0px;display:block;padding:0px}.list--hide{block-size:0px;overflow:hidden}.chip{margin-block:calc(var(--calcite-combobox-item-spacing-unit-s) / 4);margin-inline:0 var(--calcite-combobox-item-spacing-unit-s);max-inline-size:100%}.chip--active{background-color:var(--calcite-ui-foreground-3)}.item{display:block}::slotted(input[slot=hidden-form-input]){margin:0 !important;opacity:0 !important;outline:none !important;padding:0 !important;position:absolute !important;inset:0 !important;transform:none !important;-webkit-appearance:none !important;z-index:-1 !important}";
  21. const isGroup = (el) => el.tagName === ComboboxItemGroup;
  22. const itemUidPrefix = "combobox-item-";
  23. const chipUidPrefix = "combobox-chip-";
  24. const labelUidPrefix = "combobox-label-";
  25. const listboxUidPrefix = "combobox-listbox-";
  26. const inputUidPrefix = "combobox-input-";
  27. const Combobox = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
  28. constructor() {
  29. super();
  30. this.__registerHost();
  31. this.__attachShadow();
  32. this.calciteLookupChange = createEvent(this, "calciteLookupChange", 6);
  33. this.calciteComboboxChange = createEvent(this, "calciteComboboxChange", 6);
  34. this.calciteComboboxFilterChange = createEvent(this, "calciteComboboxFilterChange", 6);
  35. this.calciteComboboxChipDismiss = createEvent(this, "calciteComboboxChipDismiss", 6);
  36. this.calciteComboboxBeforeClose = createEvent(this, "calciteComboboxBeforeClose", 6);
  37. this.calciteComboboxClose = createEvent(this, "calciteComboboxClose", 6);
  38. this.calciteComboboxBeforeOpen = createEvent(this, "calciteComboboxBeforeOpen", 6);
  39. this.calciteComboboxOpen = createEvent(this, "calciteComboboxOpen", 6);
  40. //--------------------------------------------------------------------------
  41. //
  42. // Public Properties
  43. //
  44. //--------------------------------------------------------------------------
  45. /**
  46. * When `true`, displays and positions the component.
  47. *
  48. * @deprecated use `open` instead.
  49. */
  50. this.active = false;
  51. /**When `true`, displays and positions the component. */
  52. this.open = false;
  53. /** When `true`, interaction is prevented and the component is displayed with lower opacity. */
  54. this.disabled = false;
  55. /** Specifies the maximum number of `calcite-combobox-item`s (including nested children) to display before displaying a scrollbar. */
  56. this.maxItems = 0;
  57. /**
  58. * Determines the type of positioning to use for the overlaid content.
  59. *
  60. * Using `"absolute"` will work for most cases. The component will be positioned inside of overflowing parent containers and will affect the container's layout.
  61. *
  62. * `"fixed"` should be used to escape an overflowing parent container, or when the reference element's `position` CSS property is `"fixed"`.
  63. *
  64. */
  65. this.overlayPositioning = "absolute";
  66. /**
  67. * When `true`, the component must have a value in order for the form to submit.
  68. *
  69. * @internal
  70. */
  71. this.required = false;
  72. /**
  73. * Specifies the selection mode -
  74. * `"multi"` (allow any number of selected items),
  75. * `"single"` (allow only one selection), or
  76. * `"ancestors"` (like `"multi"`, but show ancestors of selected items as selected. Only the deepest children are shown in `calcite-chip`s).
  77. */
  78. this.selectionMode = "multi";
  79. /** Specifies the size of the component. */
  80. this.scale = "m";
  81. /** The component's value(s) from the selected `calcite-combobox-item`(s). */
  82. this.value = null;
  83. /**
  84. * Accessible name for the component's remove tag when a `calcite-combobox-item` is selected.
  85. *
  86. * @default "Remove tag"
  87. */
  88. this.intlRemoveTag = TEXT.removeTag;
  89. //--------------------------------------------------------------------------
  90. //
  91. // Private State/Props
  92. //
  93. //--------------------------------------------------------------------------
  94. this.placement = defaultMenuPlacement;
  95. this.internalValueChangeFlag = false;
  96. this.items = [];
  97. this.groupItems = [];
  98. this.selectedItems = [];
  99. this.visibleItems = [];
  100. this.activeItemIndex = -1;
  101. this.activeChipIndex = -1;
  102. this.activeDescendant = "";
  103. this.text = "";
  104. this.textInput = null;
  105. this.mutationObserver = createObserver("mutation", () => this.updateItems());
  106. this.resizeObserver = createObserver("resize", () => this.setMaxScrollerHeight());
  107. this.guid = guid();
  108. this.inputHeight = 0;
  109. this.ignoreSelectedEventsFlag = false;
  110. this.openTransitionProp = "opacity";
  111. // --------------------------------------------------------------------------
  112. //
  113. // Private Methods
  114. //
  115. // --------------------------------------------------------------------------
  116. this.setFilteredPlacements = () => {
  117. const { el, flipPlacements } = this;
  118. this.filteredFlipPlacements = flipPlacements
  119. ? filterComputedPlacements(flipPlacements, el)
  120. : null;
  121. };
  122. this.getValue = () => {
  123. const items = this.selectedItems.map((item) => { var _a; return (_a = item === null || item === void 0 ? void 0 : item.value) === null || _a === void 0 ? void 0 : _a.toString(); });
  124. return (items === null || items === void 0 ? void 0 : items.length) ? (items.length > 1 ? items : items[0]) : "";
  125. };
  126. this.onLabelClick = () => {
  127. this.setFocus();
  128. };
  129. this.keydownHandler = (event) => {
  130. const { key } = event;
  131. switch (key) {
  132. case "Tab":
  133. this.activeChipIndex = -1;
  134. this.activeItemIndex = -1;
  135. if (this.allowCustomValues && this.text) {
  136. this.addCustomChip(this.text, true);
  137. event.preventDefault();
  138. }
  139. else if (this.open) {
  140. this.open = false;
  141. event.preventDefault();
  142. }
  143. break;
  144. case "ArrowLeft":
  145. this.previousChip();
  146. event.preventDefault();
  147. break;
  148. case "ArrowRight":
  149. this.nextChip();
  150. event.preventDefault();
  151. break;
  152. case "ArrowUp":
  153. event.preventDefault();
  154. this.shiftActiveItemIndex(-1);
  155. if (!this.comboboxInViewport()) {
  156. this.el.scrollIntoView();
  157. }
  158. break;
  159. case "ArrowDown":
  160. event.preventDefault();
  161. if (!this.open) {
  162. this.open = true;
  163. }
  164. this.shiftActiveItemIndex(1);
  165. if (!this.comboboxInViewport()) {
  166. this.el.scrollIntoView();
  167. }
  168. break;
  169. case " ":
  170. if (!this.textInput.value) {
  171. event.preventDefault();
  172. this.open = true;
  173. this.shiftActiveItemIndex(1);
  174. }
  175. break;
  176. case "Home":
  177. if (!this.open) {
  178. return;
  179. }
  180. event.preventDefault();
  181. this.updateActiveItemIndex(0);
  182. this.scrollToActiveItem();
  183. if (!this.comboboxInViewport()) {
  184. this.el.scrollIntoView();
  185. }
  186. break;
  187. case "End":
  188. if (!this.open) {
  189. return;
  190. }
  191. event.preventDefault();
  192. this.updateActiveItemIndex(this.visibleItems.length - 1);
  193. this.scrollToActiveItem();
  194. if (!this.comboboxInViewport()) {
  195. this.el.scrollIntoView();
  196. }
  197. break;
  198. case "Escape":
  199. this.open = false;
  200. event.preventDefault();
  201. break;
  202. case "Enter":
  203. if (this.activeItemIndex > -1) {
  204. this.toggleSelection(this.visibleItems[this.activeItemIndex]);
  205. event.preventDefault();
  206. }
  207. else if (this.activeChipIndex > -1) {
  208. this.removeActiveChip();
  209. event.preventDefault();
  210. }
  211. else if (this.allowCustomValues && this.text) {
  212. this.addCustomChip(this.text, true);
  213. event.preventDefault();
  214. }
  215. else if (!event.defaultPrevented) {
  216. if (submitForm(this)) {
  217. event.preventDefault();
  218. }
  219. }
  220. break;
  221. case "Delete":
  222. case "Backspace":
  223. if (this.activeChipIndex > -1) {
  224. event.preventDefault();
  225. this.removeActiveChip();
  226. }
  227. else if (!this.text && this.isMulti()) {
  228. event.preventDefault();
  229. this.removeLastChip();
  230. }
  231. break;
  232. }
  233. };
  234. this.toggleCloseEnd = () => {
  235. this.open = false;
  236. this.el.removeEventListener("calciteComboboxClose", this.toggleCloseEnd);
  237. };
  238. this.toggleOpenEnd = () => {
  239. this.open = false;
  240. this.el.removeEventListener("calciteComboboxOpen", this.toggleOpenEnd);
  241. };
  242. this.setMaxScrollerHeight = async () => {
  243. const { listContainerEl, open, referenceEl } = this;
  244. if (!listContainerEl || !open) {
  245. return;
  246. }
  247. await this.reposition(true);
  248. const maxScrollerHeight = this.getMaxScrollerHeight();
  249. listContainerEl.style.maxHeight = maxScrollerHeight > 0 ? `${maxScrollerHeight}px` : "";
  250. listContainerEl.style.minWidth = `${referenceEl.clientWidth}px`;
  251. await this.reposition(true);
  252. };
  253. this.calciteChipDismissHandler = (event, comboboxItem) => {
  254. this.open = false;
  255. const selection = this.items.find((item) => item === comboboxItem);
  256. if (selection) {
  257. this.toggleSelection(selection, false);
  258. }
  259. this.calciteComboboxChipDismiss.emit(event.detail);
  260. };
  261. this.clickHandler = (event) => {
  262. if (event.composedPath().some((node) => node.tagName === "CALCITE-CHIP")) {
  263. return;
  264. }
  265. this.open = !this.open;
  266. this.updateActiveItemIndex(0);
  267. this.setFocus();
  268. };
  269. this.setInactiveIfNotContained = (event) => {
  270. const composedPath = event.composedPath();
  271. if (!this.open || composedPath.includes(this.el) || composedPath.includes(this.referenceEl)) {
  272. return;
  273. }
  274. if (this.allowCustomValues && this.text.trim().length) {
  275. this.addCustomChip(this.text);
  276. }
  277. if (this.selectionMode === "single") {
  278. if (this.textInput) {
  279. this.textInput.value = "";
  280. }
  281. this.text = "";
  282. this.filterItems("");
  283. this.updateActiveItemIndex(-1);
  284. }
  285. this.open = false;
  286. };
  287. this.setFloatingEl = (el) => {
  288. this.floatingEl = el;
  289. connectFloatingUI(this, this.referenceEl, this.floatingEl);
  290. };
  291. this.setContainerEl = (el) => {
  292. this.resizeObserver.observe(el);
  293. this.listContainerEl = el;
  294. this.transitionEl = el;
  295. connectOpenCloseComponent(this);
  296. };
  297. this.setReferenceEl = (el) => {
  298. this.referenceEl = el;
  299. connectFloatingUI(this, this.referenceEl, this.floatingEl);
  300. };
  301. this.inputHandler = (event) => {
  302. const value = event.target.value;
  303. this.text = value;
  304. this.filterItems(value);
  305. if (value) {
  306. this.activeChipIndex = -1;
  307. }
  308. };
  309. this.filterItems = (() => {
  310. const find = (item, filteredData) => item &&
  311. filteredData.some(({ label, value }) => {
  312. if (isGroup(item)) {
  313. return value === item.label;
  314. }
  315. return (value === item.textLabel ||
  316. value === item.value ||
  317. label === item.textLabel ||
  318. label === item.value);
  319. });
  320. return debounce((text) => {
  321. const filteredData = filter(this.data, text);
  322. const items = this.getCombinedItems();
  323. items.forEach((item) => {
  324. const hidden = !find(item, filteredData);
  325. item.hidden = hidden;
  326. const [parent, grandparent] = item.ancestors;
  327. if (find(parent, filteredData) || find(grandparent, filteredData)) {
  328. item.hidden = false;
  329. }
  330. if (!hidden) {
  331. item.ancestors.forEach((ancestor) => (ancestor.hidden = false));
  332. }
  333. });
  334. this.visibleItems = this.getVisibleItems();
  335. this.calciteComboboxFilterChange.emit({ visibleItems: [...this.visibleItems], text: text });
  336. }, 100);
  337. })();
  338. this.internalCalciteLookupChangeEvent = () => {
  339. this.calciteLookupChange.emit(this.selectedItems);
  340. };
  341. this.emitCalciteLookupChange = debounce(this.internalCalciteLookupChangeEvent, 0);
  342. this.internalComboboxChangeEvent = () => {
  343. const { selectedItems } = this;
  344. this.calciteComboboxChange.emit({ selectedItems });
  345. };
  346. this.emitComboboxChange = debounce(this.internalComboboxChangeEvent, 0);
  347. this.updateItems = () => {
  348. this.items = this.getItems();
  349. this.groupItems = this.getGroupItems();
  350. this.data = this.getData();
  351. this.selectedItems = this.getSelectedItems();
  352. this.visibleItems = this.getVisibleItems();
  353. this.needsIcon = this.getNeedsIcon();
  354. if (!this.allowCustomValues) {
  355. this.setMaxScrollerHeight();
  356. }
  357. };
  358. this.scrollToActiveItem = () => {
  359. const activeItem = this.visibleItems[this.activeItemIndex];
  360. const height = this.calculateSingleItemHeight(activeItem);
  361. const { offsetHeight, scrollTop } = this.listContainerEl;
  362. if (offsetHeight + scrollTop < activeItem.offsetTop + height) {
  363. this.listContainerEl.scrollTop = activeItem.offsetTop - offsetHeight + height;
  364. }
  365. else if (activeItem.offsetTop < scrollTop) {
  366. this.listContainerEl.scrollTop = activeItem.offsetTop;
  367. }
  368. };
  369. this.comboboxFocusHandler = () => {
  370. var _a;
  371. (_a = this.textInput) === null || _a === void 0 ? void 0 : _a.focus();
  372. };
  373. this.comboboxBlurHandler = (event) => {
  374. this.setInactiveIfNotContained(event);
  375. };
  376. }
  377. activeHandler(value) {
  378. if (this.disabled) {
  379. this.active = false;
  380. this.open = false;
  381. return;
  382. }
  383. this.open = value;
  384. }
  385. openHandler(value) {
  386. if (!value) {
  387. updateAfterClose(this.floatingEl);
  388. }
  389. if (this.disabled) {
  390. this.active = false;
  391. this.open = false;
  392. return;
  393. }
  394. this.active = value;
  395. this.setMaxScrollerHeight();
  396. }
  397. handleDisabledChange(value) {
  398. if (!value) {
  399. this.active = false;
  400. this.open = false;
  401. }
  402. }
  403. maxItemsHandler() {
  404. this.setMaxScrollerHeight();
  405. }
  406. overlayPositioningHandler() {
  407. this.reposition(true);
  408. }
  409. valueHandler(value) {
  410. if (!this.internalValueChangeFlag) {
  411. const items = this.getItems();
  412. if (Array.isArray(value)) {
  413. items.forEach((item) => (item.selected = value.includes(item.value)));
  414. }
  415. else if (value) {
  416. items.forEach((item) => (item.selected = value === item.value));
  417. }
  418. else {
  419. items.forEach((item) => (item.selected = false));
  420. }
  421. this.updateItems();
  422. }
  423. }
  424. flipPlacementsHandler() {
  425. this.setFilteredPlacements();
  426. this.reposition(true);
  427. }
  428. //--------------------------------------------------------------------------
  429. //
  430. // Event Listeners
  431. //
  432. //--------------------------------------------------------------------------
  433. documentClickHandler(event) {
  434. if (!isPrimaryPointerButton(event)) {
  435. return;
  436. }
  437. this.setInactiveIfNotContained(event);
  438. }
  439. calciteComboboxItemChangeHandler(event) {
  440. if (this.ignoreSelectedEventsFlag) {
  441. return;
  442. }
  443. const target = event.target;
  444. const newIndex = this.visibleItems.indexOf(target);
  445. this.updateActiveItemIndex(newIndex);
  446. this.toggleSelection(target, target.selected);
  447. }
  448. //--------------------------------------------------------------------------
  449. //
  450. // Public Methods
  451. //
  452. //--------------------------------------------------------------------------
  453. /**
  454. * Updates the position of the component.
  455. *
  456. * @param delayed
  457. */
  458. async reposition(delayed = false) {
  459. const { floatingEl, referenceEl, placement, overlayPositioning, filteredFlipPlacements } = this;
  460. return reposition(this, {
  461. floatingEl,
  462. referenceEl,
  463. overlayPositioning,
  464. placement,
  465. flipPlacements: filteredFlipPlacements,
  466. type: "menu"
  467. }, delayed);
  468. }
  469. /** Sets focus on the component. */
  470. async setFocus() {
  471. var _a;
  472. (_a = this.textInput) === null || _a === void 0 ? void 0 : _a.focus();
  473. this.activeChipIndex = -1;
  474. this.activeItemIndex = -1;
  475. }
  476. // --------------------------------------------------------------------------
  477. //
  478. // Lifecycle
  479. //
  480. // --------------------------------------------------------------------------
  481. connectedCallback() {
  482. var _a;
  483. this.internalValueChangeFlag = true;
  484. this.value = this.getValue();
  485. this.internalValueChangeFlag = false;
  486. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.observe(this.el, { childList: true, subtree: true });
  487. connectLabel(this);
  488. connectForm(this);
  489. connectOpenCloseComponent(this);
  490. this.setFilteredPlacements();
  491. this.reposition(true);
  492. if (this.active) {
  493. this.activeHandler(this.active);
  494. }
  495. if (this.open) {
  496. this.openHandler(this.open);
  497. }
  498. }
  499. componentWillLoad() {
  500. this.updateItems();
  501. }
  502. componentDidLoad() {
  503. afterConnectDefaultValueSet(this, this.getValue());
  504. this.reposition(true);
  505. }
  506. componentDidRender() {
  507. if (this.el.offsetHeight !== this.inputHeight) {
  508. this.reposition(true);
  509. this.inputHeight = this.el.offsetHeight;
  510. }
  511. updateHostInteraction(this);
  512. }
  513. disconnectedCallback() {
  514. var _a, _b;
  515. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
  516. (_b = this.resizeObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
  517. disconnectLabel(this);
  518. disconnectForm(this);
  519. disconnectFloatingUI(this, this.referenceEl, this.floatingEl);
  520. disconnectOpenCloseComponent(this);
  521. }
  522. selectedItemsHandler() {
  523. this.internalValueChangeFlag = true;
  524. this.value = this.getValue();
  525. this.internalValueChangeFlag = false;
  526. }
  527. /** when search text is cleared, reset active to */
  528. textHandler() {
  529. this.updateActiveItemIndex(-1);
  530. }
  531. comboboxInViewport() {
  532. const bounding = this.el.getBoundingClientRect();
  533. return (bounding.top >= 0 &&
  534. bounding.left >= 0 &&
  535. bounding.right <= (window.innerWidth || document.documentElement.clientWidth) &&
  536. bounding.bottom <= (window.innerHeight || document.documentElement.clientHeight));
  537. }
  538. onBeforeOpen() {
  539. this.calciteComboboxBeforeOpen.emit();
  540. }
  541. onOpen() {
  542. this.calciteComboboxOpen.emit();
  543. }
  544. onBeforeClose() {
  545. this.calciteComboboxBeforeClose.emit();
  546. }
  547. onClose() {
  548. this.calciteComboboxClose.emit();
  549. }
  550. getMaxScrollerHeight() {
  551. const items = this.getCombinedItems().filter((item) => !item.hidden);
  552. const { maxItems } = this;
  553. let itemsToProcess = 0;
  554. let maxScrollerHeight = 0;
  555. if (items.length > maxItems) {
  556. items.forEach((item) => {
  557. if (itemsToProcess < maxItems && maxItems > 0) {
  558. const height = this.calculateSingleItemHeight(item);
  559. if (height > 0) {
  560. maxScrollerHeight += height;
  561. itemsToProcess++;
  562. }
  563. }
  564. });
  565. }
  566. return maxScrollerHeight;
  567. }
  568. calculateSingleItemHeight(item) {
  569. let height = item.offsetHeight;
  570. // if item has children items, don't count their height twice
  571. const children = Array.from(item.querySelectorAll(ComboboxChildSelector));
  572. children
  573. .map((child) => child === null || child === void 0 ? void 0 : child.offsetHeight)
  574. .forEach((offsetHeight) => {
  575. height -= offsetHeight;
  576. });
  577. return height;
  578. }
  579. getCombinedItems() {
  580. return [...this.groupItems, ...this.items];
  581. }
  582. toggleSelection(item, value = !item.selected) {
  583. if (!item) {
  584. return;
  585. }
  586. if (this.isMulti()) {
  587. item.selected = value;
  588. this.updateAncestors(item);
  589. this.selectedItems = this.getSelectedItems();
  590. this.emitCalciteLookupChange();
  591. this.emitComboboxChange();
  592. this.resetText();
  593. this.filterItems("");
  594. }
  595. else {
  596. this.ignoreSelectedEventsFlag = true;
  597. this.items.forEach((el) => (el.selected = el === item ? value : false));
  598. this.ignoreSelectedEventsFlag = false;
  599. this.selectedItems = this.getSelectedItems();
  600. this.emitComboboxChange();
  601. if (this.textInput) {
  602. this.textInput.value = item.textLabel;
  603. }
  604. this.open = false;
  605. this.updateActiveItemIndex(-1);
  606. this.resetText();
  607. this.filterItems("");
  608. }
  609. }
  610. updateAncestors(item) {
  611. if (this.selectionMode !== "ancestors") {
  612. return;
  613. }
  614. const ancestors = getItemAncestors(item);
  615. const children = getItemChildren(item);
  616. if (item.selected) {
  617. ancestors.forEach((el) => {
  618. el.selected = true;
  619. });
  620. }
  621. else {
  622. children.forEach((el) => (el.selected = false));
  623. [...ancestors].forEach((el) => {
  624. if (!hasActiveChildren(el)) {
  625. el.selected = false;
  626. }
  627. });
  628. }
  629. }
  630. getVisibleItems() {
  631. return this.items.filter((item) => !item.hidden);
  632. }
  633. getSelectedItems() {
  634. if (!this.isMulti()) {
  635. const match = this.items.find(({ selected }) => selected);
  636. return match ? [match] : [];
  637. }
  638. return (this.items
  639. .filter((item) => item.selected && (this.selectionMode !== "ancestors" || !hasActiveChildren(item)))
  640. /** Preserve order of entered tags */
  641. .sort((a, b) => {
  642. const aIdx = this.selectedItems.indexOf(a);
  643. const bIdx = this.selectedItems.indexOf(b);
  644. if (aIdx > -1 && bIdx > -1) {
  645. return aIdx - bIdx;
  646. }
  647. return bIdx - aIdx;
  648. }));
  649. }
  650. getData() {
  651. return this.items.map((item) => ({
  652. constant: item.constant,
  653. filterDisabled: item.filterDisabled,
  654. value: item.value,
  655. label: item.textLabel
  656. }));
  657. }
  658. getNeedsIcon() {
  659. return this.selectionMode === "single" && this.items.some((item) => item.icon);
  660. }
  661. resetText() {
  662. if (this.textInput) {
  663. this.textInput.value = "";
  664. }
  665. this.text = "";
  666. }
  667. getItems() {
  668. const items = Array.from(this.el.querySelectorAll(ComboboxItem));
  669. return items.filter((item) => !item.disabled);
  670. }
  671. getGroupItems() {
  672. return Array.from(this.el.querySelectorAll(ComboboxItemGroup));
  673. }
  674. addCustomChip(value, focus) {
  675. const existingItem = this.items.find((el) => el.textLabel === value);
  676. if (existingItem) {
  677. this.toggleSelection(existingItem, true);
  678. }
  679. else {
  680. if (!this.isMulti()) {
  681. this.toggleSelection(this.selectedItems[this.selectedItems.length - 1], false);
  682. }
  683. const item = document.createElement(ComboboxItem);
  684. item.value = value;
  685. item.textLabel = value;
  686. item.selected = true;
  687. this.el.appendChild(item);
  688. this.resetText();
  689. if (focus) {
  690. this.setFocus();
  691. }
  692. this.updateItems();
  693. this.filterItems("");
  694. this.emitCalciteLookupChange();
  695. this.emitComboboxChange();
  696. }
  697. }
  698. removeActiveChip() {
  699. this.toggleSelection(this.selectedItems[this.activeChipIndex], false);
  700. this.setFocus();
  701. }
  702. removeLastChip() {
  703. this.toggleSelection(this.selectedItems[this.selectedItems.length - 1], false);
  704. this.setFocus();
  705. }
  706. previousChip() {
  707. if (this.text) {
  708. return;
  709. }
  710. const length = this.selectedItems.length - 1;
  711. const active = this.activeChipIndex;
  712. this.activeChipIndex = active === -1 ? length : Math.max(active - 1, 0);
  713. this.updateActiveItemIndex(-1);
  714. this.focusChip();
  715. }
  716. nextChip() {
  717. if (this.text || this.activeChipIndex === -1) {
  718. return;
  719. }
  720. const last = this.selectedItems.length - 1;
  721. const newIndex = this.activeChipIndex + 1;
  722. if (newIndex > last) {
  723. this.activeChipIndex = -1;
  724. this.setFocus();
  725. }
  726. else {
  727. this.activeChipIndex = newIndex;
  728. this.focusChip();
  729. }
  730. this.updateActiveItemIndex(-1);
  731. }
  732. focusChip() {
  733. var _a;
  734. const guid = (_a = this.selectedItems[this.activeChipIndex]) === null || _a === void 0 ? void 0 : _a.guid;
  735. const chip = guid
  736. ? this.referenceEl.querySelector(`#${chipUidPrefix}${guid}`)
  737. : null;
  738. chip === null || chip === void 0 ? void 0 : chip.setFocus();
  739. }
  740. shiftActiveItemIndex(delta) {
  741. const { length } = this.visibleItems;
  742. const newIndex = (this.activeItemIndex + length + delta) % length;
  743. this.updateActiveItemIndex(newIndex);
  744. this.scrollToActiveItem();
  745. }
  746. updateActiveItemIndex(index) {
  747. var _a;
  748. this.activeItemIndex = index;
  749. let activeDescendant = null;
  750. this.visibleItems.forEach((el, i) => {
  751. if (i === index) {
  752. el.active = true;
  753. activeDescendant = `${itemUidPrefix}${el.guid}`;
  754. }
  755. else {
  756. el.active = false;
  757. }
  758. });
  759. this.activeDescendant = activeDescendant;
  760. if (this.activeItemIndex > -1) {
  761. this.activeChipIndex = -1;
  762. (_a = this.textInput) === null || _a === void 0 ? void 0 : _a.focus();
  763. }
  764. }
  765. isMulti() {
  766. return this.selectionMode !== "single";
  767. }
  768. //--------------------------------------------------------------------------
  769. //
  770. // Render Methods
  771. //
  772. //--------------------------------------------------------------------------
  773. renderChips() {
  774. const { activeChipIndex, scale, selectionMode, intlRemoveTag } = this;
  775. return this.selectedItems.map((item, i) => {
  776. const chipClasses = {
  777. chip: true,
  778. "chip--active": activeChipIndex === i
  779. };
  780. const ancestors = [...getItemAncestors(item)].reverse();
  781. const pathLabel = [...ancestors, item].map((el) => el.textLabel);
  782. const label = selectionMode !== "ancestors" ? item.textLabel : pathLabel.join(" / ");
  783. return (h("calcite-chip", { class: chipClasses, dismissLabel: intlRemoveTag, dismissible: true, icon: item.icon, id: item.guid ? `${chipUidPrefix}${item.guid}` : null, key: item.textLabel, onCalciteChipDismiss: (event) => this.calciteChipDismissHandler(event, item), scale: scale, title: label, value: item.value }, label));
  784. });
  785. }
  786. renderInput() {
  787. const { guid, active, disabled, placeholder, selectionMode, selectedItems, open } = this;
  788. const single = selectionMode === "single";
  789. const selectedItem = selectedItems[0];
  790. const showLabel = !(open || active) && single && !!selectedItem;
  791. return (h("span", { class: {
  792. "input-wrap": true,
  793. "input-wrap--single": single
  794. } }, showLabel && (h("span", { class: {
  795. label: true,
  796. "label--icon": !!(selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.icon)
  797. }, key: "label" }, selectedItem.textLabel)), h("input", { "aria-activedescendant": this.activeDescendant, "aria-autocomplete": "list", "aria-controls": `${listboxUidPrefix}${guid}`, "aria-label": getLabelText(this), class: {
  798. input: true,
  799. "input--single": true,
  800. "input--transparent": this.activeChipIndex > -1,
  801. "input--hidden": showLabel,
  802. "input--icon": !!this.placeholderIcon
  803. }, disabled: disabled, id: `${inputUidPrefix}${guid}`, key: "input", onBlur: this.comboboxBlurHandler, onFocus: this.comboboxFocusHandler, onInput: this.inputHandler, placeholder: placeholder, ref: (el) => (this.textInput = el), type: "text" })));
  804. }
  805. renderListBoxOptions() {
  806. return this.visibleItems.map((item) => (h("li", { "aria-selected": toAriaBoolean(item.selected), id: item.guid ? `${itemUidPrefix}${item.guid}` : null, role: "option", tabindex: "-1" }, item.textLabel)));
  807. }
  808. renderFloatingUIContainer() {
  809. const { active, setFloatingEl, setContainerEl, open } = this;
  810. const classes = {
  811. "list-container": true,
  812. [FloatingCSS.animation]: true,
  813. [FloatingCSS.animationActive]: open || active
  814. };
  815. return (h("div", { "aria-hidden": "true", class: {
  816. "floating-ui-container": true,
  817. "floating-ui-container--active": open || active
  818. }, ref: setFloatingEl }, h("div", { class: classes, ref: setContainerEl }, h("ul", { class: { list: true, "list--hide": !(open || active) } }, h("slot", null)))));
  819. }
  820. renderIconStart() {
  821. const { selectedItems, placeholderIcon, selectionMode } = this;
  822. const selectedItem = selectedItems[0];
  823. const selectedIcon = selectedItem === null || selectedItem === void 0 ? void 0 : selectedItem.icon;
  824. const singleSelectionMode = selectionMode === "single";
  825. const iconAtStart = !this.open && selectedItem
  826. ? !!selectedIcon && singleSelectionMode
  827. : !!this.placeholderIcon && (!selectedItem || singleSelectionMode);
  828. return (iconAtStart && (h("span", { class: "icon-start" }, h("calcite-icon", { class: "selected-icon", icon: !this.open && selectedItem ? selectedIcon : placeholderIcon, scale: "s" }))));
  829. }
  830. renderIconEnd() {
  831. const { active, open } = this;
  832. return (h("span", { class: "icon-end" }, h("calcite-icon", { icon: active || open ? "chevron-up" : "chevron-down", scale: "s" })));
  833. }
  834. render() {
  835. const { active, guid, label, open } = this;
  836. const single = this.selectionMode === "single";
  837. return (h(Host, null, h("div", { "aria-autocomplete": "list", "aria-controls": `${listboxUidPrefix}${guid}`, "aria-expanded": toAriaBoolean(open || active), "aria-haspopup": "listbox", "aria-labelledby": `${labelUidPrefix}${guid}`, "aria-live": "polite", "aria-owns": `${listboxUidPrefix}${guid}`, class: {
  838. wrapper: true,
  839. "wrapper--single": single || !this.selectedItems.length,
  840. "wrapper--active": open || active
  841. }, onClick: this.clickHandler, onKeyDown: this.keydownHandler, ref: this.setReferenceEl, role: "combobox" }, h("div", { class: "grid-input" }, this.renderIconStart(), !single && this.renderChips(), h("label", { class: "screen-readers-only", htmlFor: `${inputUidPrefix}${guid}`, id: `${labelUidPrefix}${guid}` }, label), this.renderInput()), this.renderIconEnd()), h("ul", { "aria-labelledby": `${labelUidPrefix}${guid}`, "aria-multiselectable": "true", class: "screen-readers-only", id: `${listboxUidPrefix}${guid}`, role: "listbox", tabIndex: -1 }, this.renderListBoxOptions()), this.renderFloatingUIContainer(), h(HiddenFormInputSlot, { component: this })));
  842. }
  843. get el() { return this; }
  844. static get watchers() { return {
  845. "active": ["activeHandler"],
  846. "open": ["openHandler"],
  847. "disabled": ["handleDisabledChange"],
  848. "maxItems": ["maxItemsHandler"],
  849. "overlayPositioning": ["overlayPositioningHandler"],
  850. "value": ["valueHandler"],
  851. "flipPlacements": ["flipPlacementsHandler"],
  852. "selectedItems": ["selectedItemsHandler"],
  853. "text": ["textHandler"]
  854. }; }
  855. static get style() { return comboboxCss; }
  856. }, [1, "calcite-combobox", {
  857. "active": [1540],
  858. "open": [1540],
  859. "disabled": [516],
  860. "label": [1],
  861. "placeholder": [1],
  862. "placeholderIcon": [513, "placeholder-icon"],
  863. "maxItems": [514, "max-items"],
  864. "name": [513],
  865. "allowCustomValues": [516, "allow-custom-values"],
  866. "overlayPositioning": [513, "overlay-positioning"],
  867. "required": [516],
  868. "selectionMode": [513, "selection-mode"],
  869. "scale": [513],
  870. "value": [1025],
  871. "intlRemoveTag": [1, "intl-remove-tag"],
  872. "flipPlacements": [16],
  873. "items": [32],
  874. "groupItems": [32],
  875. "selectedItems": [32],
  876. "visibleItems": [32],
  877. "needsIcon": [32],
  878. "activeItemIndex": [32],
  879. "activeChipIndex": [32],
  880. "activeDescendant": [32],
  881. "text": [32],
  882. "reposition": [64],
  883. "setFocus": [64]
  884. }, [[5, "pointerdown", "documentClickHandler"], [0, "calciteComboboxItemChange", "calciteComboboxItemChangeHandler"]]]);
  885. function defineCustomElement$1() {
  886. if (typeof customElements === "undefined") {
  887. return;
  888. }
  889. const components = ["calcite-combobox", "calcite-chip", "calcite-icon"];
  890. components.forEach(tagName => { switch (tagName) {
  891. case "calcite-combobox":
  892. if (!customElements.get(tagName)) {
  893. customElements.define(tagName, Combobox);
  894. }
  895. break;
  896. case "calcite-chip":
  897. if (!customElements.get(tagName)) {
  898. defineCustomElement$3();
  899. }
  900. break;
  901. case "calcite-icon":
  902. if (!customElements.get(tagName)) {
  903. defineCustomElement$2();
  904. }
  905. break;
  906. } });
  907. }
  908. defineCustomElement$1();
  909. const CalciteCombobox = Combobox;
  910. const defineCustomElement = defineCustomElement$1;
  911. export { CalciteCombobox, defineCustomElement };