dropdown.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500
  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 { t as toAriaBoolean, i as isPrimaryPointerButton, f as focusElement } from './dom.js';
  8. import { d as defaultMenuPlacement, f as filterComputedPlacements, c as connectFloatingUI, u as updateAfterClose, a as disconnectFloatingUI, F as FloatingCSS, r as reposition } from './floating-ui.js';
  9. import { c as createObserver } from './observers.js';
  10. import { u as updateHostInteraction } from './interactive.js';
  11. import { c as connectOpenCloseComponent, d as disconnectOpenCloseComponent } from './openCloseComponent.js';
  12. import { g as guid } from './guid.js';
  13. import { i as isActivationKey } from './key.js';
  14. const SLOTS = {
  15. dropdownTrigger: "dropdown-trigger"
  16. };
  17. const dropdownCss = "@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{display:inline-flex;flex:0 1 auto}:host([disabled]) ::slotted([calcite-hydrated][disabled]),:host([disabled]) [calcite-hydrated][disabled]{opacity:1}:host .calcite-dropdown-wrapper{display:block;position:absolute;z-index:900;visibility:hidden;pointer-events:none;inline-size:0;block-size:0;overflow:hidden}.calcite-dropdown-wrapper .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}.calcite-dropdown-wrapper[data-placement^=bottom] .calcite-floating-ui-anim{transform:translateY(-5px)}.calcite-dropdown-wrapper[data-placement^=top] .calcite-floating-ui-anim{transform:translateY(5px)}.calcite-dropdown-wrapper[data-placement^=left] .calcite-floating-ui-anim{transform:translateX(5px)}.calcite-dropdown-wrapper[data-placement^=right] .calcite-floating-ui-anim{transform:translateX(-5px)}.calcite-dropdown-wrapper[data-placement] .calcite-floating-ui-anim--active{opacity:1;visibility:visible;transform:translate(0)}:host([open]) .calcite-dropdown-wrapper{pointer-events:initial;visibility:visible;inline-size:unset;block-size:unset;overflow:unset}:host .calcite-dropdown-content{max-block-size:45vh;inline-size:auto;overflow-y:auto;overflow-x:hidden;background-color:var(--calcite-ui-foreground-1);inline-size:var(--calcite-dropdown-width)}.calcite-dropdown-trigger-container{position:relative;display:flex;flex:1 1 auto}@media (forced-colors: active){:host([open]) .calcite-dropdown-wrapper{border:1px solid canvasText}}:host([width=s]){--calcite-dropdown-width:12rem}:host([width=m]){--calcite-dropdown-width:14rem}:host([width=l]){--calcite-dropdown-width:16rem}";
  18. const Dropdown = /*@__PURE__*/ proxyCustomElement(class extends HTMLElement {
  19. constructor() {
  20. super();
  21. this.__registerHost();
  22. this.__attachShadow();
  23. this.calciteDropdownSelect = createEvent(this, "calciteDropdownSelect", 6);
  24. this.calciteDropdownBeforeClose = createEvent(this, "calciteDropdownBeforeClose", 6);
  25. this.calciteDropdownClose = createEvent(this, "calciteDropdownClose", 6);
  26. this.calciteDropdownBeforeOpen = createEvent(this, "calciteDropdownBeforeOpen", 6);
  27. this.calciteDropdownOpen = createEvent(this, "calciteDropdownOpen", 6);
  28. //--------------------------------------------------------------------------
  29. //
  30. // Public Properties
  31. //
  32. //--------------------------------------------------------------------------
  33. /**
  34. * Opens or closes the dropdown
  35. *
  36. * @deprecated use open instead.
  37. */
  38. this.active = false;
  39. /** When true, opens the dropdown */
  40. this.open = false;
  41. /**
  42. allow the dropdown to remain open after a selection is made
  43. if the selection-mode of the selected item's containing group is "none", the dropdown will always close
  44. */
  45. this.disableCloseOnSelect = false;
  46. /** is the dropdown disabled */
  47. this.disabled = false;
  48. /**
  49. specify the maximum number of calcite-dropdown-items to display before showing the scroller, must be greater than 0 -
  50. this value does not include groupTitles passed to calcite-dropdown-group
  51. */
  52. this.maxItems = 0;
  53. /**
  54. * Determines the type of positioning to use for the overlaid content.
  55. *
  56. * Using `"absolute"` will work for most cases. The component will be positioned inside of overflowing parent containers and will affect the container's layout.
  57. *
  58. * `"fixed"` should be used to escape an overflowing parent container, or when the reference element's `position` CSS property is `"fixed"`.
  59. *
  60. */
  61. this.overlayPositioning = "absolute";
  62. /**
  63. * Determines where the dropdown will be positioned relative to the button.
  64. *
  65. * @default "bottom-start"
  66. */
  67. this.placement = defaultMenuPlacement;
  68. /** specify the scale of dropdown, defaults to m */
  69. this.scale = "m";
  70. /**
  71. * **read-only** The currently selected items
  72. *
  73. * @readonly
  74. */
  75. this.selectedItems = [];
  76. /** specify whether the dropdown is opened by hover or click of a trigger element */
  77. this.type = "click";
  78. this.items = [];
  79. this.groups = [];
  80. this.mutationObserver = createObserver("mutation", () => this.updateItems());
  81. this.resizeObserver = createObserver("resize", (entries) => this.resizeObserverCallback(entries));
  82. this.openTransitionProp = "visibility";
  83. this.guid = `calcite-dropdown-${guid()}`;
  84. this.defaultAssignedElements = [];
  85. //--------------------------------------------------------------------------
  86. //
  87. // Private Methods
  88. //
  89. //--------------------------------------------------------------------------
  90. this.slotChangeHandler = (event) => {
  91. this.defaultAssignedElements = event.target.assignedElements({
  92. flatten: true
  93. });
  94. this.updateItems();
  95. };
  96. this.setFilteredPlacements = () => {
  97. const { el, flipPlacements } = this;
  98. this.filteredFlipPlacements = flipPlacements
  99. ? filterComputedPlacements(flipPlacements, el)
  100. : null;
  101. };
  102. this.updateTriggers = (event) => {
  103. this.triggers = event.target.assignedElements({
  104. flatten: true
  105. });
  106. this.reposition(true);
  107. };
  108. this.updateItems = () => {
  109. this.items = this.groups
  110. .map((group) => Array.from(group === null || group === void 0 ? void 0 : group.querySelectorAll("calcite-dropdown-item")))
  111. .reduce((previousValue, currentValue) => [...previousValue, ...currentValue], []);
  112. this.updateSelectedItems();
  113. this.reposition(true);
  114. };
  115. this.updateGroups = (event) => {
  116. const groups = event.target
  117. .assignedElements({ flatten: true })
  118. .filter((el) => el === null || el === void 0 ? void 0 : el.matches("calcite-dropdown-group"));
  119. this.groups = groups;
  120. this.updateItems();
  121. };
  122. this.resizeObserverCallback = (entries) => {
  123. entries.forEach((entry) => {
  124. const { target } = entry;
  125. if (target === this.referenceEl) {
  126. this.setDropdownWidth();
  127. }
  128. else if (target === this.scrollerEl) {
  129. this.setMaxScrollerHeight();
  130. }
  131. });
  132. };
  133. this.setDropdownWidth = () => {
  134. const { referenceEl, scrollerEl } = this;
  135. const referenceElWidth = referenceEl === null || referenceEl === void 0 ? void 0 : referenceEl.clientWidth;
  136. if (!referenceElWidth || !scrollerEl) {
  137. return;
  138. }
  139. scrollerEl.style.minWidth = `${referenceElWidth}px`;
  140. };
  141. this.setMaxScrollerHeight = () => {
  142. const { scrollerEl } = this;
  143. if (!scrollerEl) {
  144. return;
  145. }
  146. this.reposition(true);
  147. const maxScrollerHeight = this.getMaxScrollerHeight();
  148. scrollerEl.style.maxHeight = maxScrollerHeight > 0 ? `${maxScrollerHeight}px` : "";
  149. this.reposition(true);
  150. };
  151. this.setScrollerAndTransitionEl = (el) => {
  152. this.resizeObserver.observe(el);
  153. this.scrollerEl = el;
  154. this.transitionEl = el;
  155. connectOpenCloseComponent(this);
  156. };
  157. this.setReferenceEl = (el) => {
  158. this.referenceEl = el;
  159. connectFloatingUI(this, this.referenceEl, this.floatingEl);
  160. this.resizeObserver.observe(el);
  161. };
  162. this.setFloatingEl = (el) => {
  163. this.floatingEl = el;
  164. connectFloatingUI(this, this.referenceEl, this.floatingEl);
  165. };
  166. this.keyDownHandler = (event) => {
  167. const target = event.target;
  168. if (target !== this.referenceEl) {
  169. return;
  170. }
  171. const { defaultPrevented, key } = event;
  172. if (defaultPrevented) {
  173. return;
  174. }
  175. if (this.open) {
  176. if (key === "Escape") {
  177. this.closeCalciteDropdown();
  178. event.preventDefault();
  179. return;
  180. }
  181. else if (event.shiftKey && key === "Tab") {
  182. this.closeCalciteDropdown();
  183. event.preventDefault();
  184. return;
  185. }
  186. }
  187. if (isActivationKey(key)) {
  188. this.openCalciteDropdown();
  189. event.preventDefault();
  190. }
  191. else if (key === "Escape") {
  192. this.closeCalciteDropdown();
  193. event.preventDefault();
  194. }
  195. };
  196. this.focusOnFirstActiveOrFirstItem = () => {
  197. this.getFocusableElement(this.items.find((item) => item.selected) || this.items[0]);
  198. };
  199. this.toggleOpenEnd = () => {
  200. this.focusOnFirstActiveOrFirstItem();
  201. this.el.removeEventListener("calciteDropdownOpen", this.toggleOpenEnd);
  202. };
  203. this.openCalciteDropdown = () => {
  204. this.open = !this.open;
  205. if (this.open) {
  206. this.el.addEventListener("calciteDropdownOpen", this.toggleOpenEnd);
  207. }
  208. };
  209. }
  210. activeHandler(value) {
  211. this.open = value;
  212. }
  213. openHandler(value) {
  214. if (!this.disabled) {
  215. if (value) {
  216. this.reposition(true);
  217. }
  218. else {
  219. updateAfterClose(this.floatingEl);
  220. }
  221. this.active = value;
  222. return;
  223. }
  224. if (!value) {
  225. updateAfterClose(this.floatingEl);
  226. }
  227. this.open = false;
  228. }
  229. handleDisabledChange(value) {
  230. if (!value) {
  231. this.open = false;
  232. }
  233. }
  234. flipPlacementsHandler() {
  235. this.setFilteredPlacements();
  236. this.reposition(true);
  237. }
  238. maxItemsHandler() {
  239. this.setMaxScrollerHeight();
  240. }
  241. overlayPositioningHandler() {
  242. this.reposition(true);
  243. }
  244. placementHandler() {
  245. this.reposition(true);
  246. }
  247. //--------------------------------------------------------------------------
  248. //
  249. // Lifecycle
  250. //
  251. //--------------------------------------------------------------------------
  252. connectedCallback() {
  253. var _a;
  254. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.observe(this.el, { childList: true, subtree: true });
  255. this.setFilteredPlacements();
  256. this.reposition(true);
  257. if (this.open) {
  258. this.openHandler(this.open);
  259. }
  260. if (this.active) {
  261. this.activeHandler(this.active);
  262. }
  263. connectOpenCloseComponent(this);
  264. }
  265. componentDidLoad() {
  266. this.reposition(true);
  267. }
  268. componentDidRender() {
  269. updateHostInteraction(this);
  270. }
  271. disconnectedCallback() {
  272. var _a, _b;
  273. (_a = this.mutationObserver) === null || _a === void 0 ? void 0 : _a.disconnect();
  274. disconnectFloatingUI(this, this.referenceEl, this.floatingEl);
  275. (_b = this.resizeObserver) === null || _b === void 0 ? void 0 : _b.disconnect();
  276. disconnectOpenCloseComponent(this);
  277. }
  278. render() {
  279. const { open, guid } = this;
  280. return (h(Host, null, h("div", { class: "calcite-dropdown-trigger-container", id: `${guid}-menubutton`, onClick: this.openCalciteDropdown, onKeyDown: this.keyDownHandler, ref: this.setReferenceEl }, h("slot", { "aria-controls": `${guid}-menu`, "aria-expanded": toAriaBoolean(open), "aria-haspopup": "menu", name: SLOTS.dropdownTrigger, onSlotchange: this.updateTriggers })), h("div", { "aria-hidden": toAriaBoolean(!open), class: "calcite-dropdown-wrapper", ref: this.setFloatingEl }, h("div", { "aria-labelledby": `${guid}-menubutton`, class: {
  281. ["calcite-dropdown-content"]: true,
  282. [FloatingCSS.animation]: true,
  283. [FloatingCSS.animationActive]: open
  284. }, id: `${guid}-menu`, ref: this.setScrollerAndTransitionEl, role: "menu" }, h("slot", { onSlotchange: this.updateGroups })))));
  285. }
  286. //--------------------------------------------------------------------------
  287. //
  288. // Public Methods
  289. //
  290. //--------------------------------------------------------------------------
  291. /**
  292. * Updates the position of the component.
  293. *
  294. * @param delayed
  295. */
  296. async reposition(delayed = false) {
  297. const { floatingEl, referenceEl, placement, overlayPositioning, filteredFlipPlacements } = this;
  298. return reposition(this, {
  299. floatingEl,
  300. referenceEl,
  301. overlayPositioning,
  302. placement,
  303. flipPlacements: filteredFlipPlacements,
  304. type: "menu"
  305. }, delayed);
  306. }
  307. closeCalciteDropdownOnClick(event) {
  308. if (!isPrimaryPointerButton(event) || !this.open || event.composedPath().includes(this.el)) {
  309. return;
  310. }
  311. this.closeCalciteDropdown(false);
  312. }
  313. closeCalciteDropdownOnEvent(event) {
  314. this.closeCalciteDropdown();
  315. event.stopPropagation();
  316. }
  317. closeCalciteDropdownOnOpenEvent(event) {
  318. if (event.composedPath().includes(this.el)) {
  319. return;
  320. }
  321. this.open = false;
  322. }
  323. mouseEnterHandler() {
  324. if (this.type === "hover") {
  325. this.openCalciteDropdown();
  326. }
  327. }
  328. mouseLeaveHandler() {
  329. if (this.type === "hover") {
  330. this.closeCalciteDropdown();
  331. }
  332. }
  333. calciteInternalDropdownItemKeyEvent(event) {
  334. const { keyboardEvent } = event.detail;
  335. // handle edge
  336. const target = keyboardEvent.target;
  337. const itemToFocus = target.nodeName !== "A" ? target : target.parentNode;
  338. const isFirstItem = this.itemIndex(itemToFocus) === 0;
  339. const isLastItem = this.itemIndex(itemToFocus) === this.items.length - 1;
  340. switch (keyboardEvent.key) {
  341. case "Tab":
  342. if (isLastItem && !keyboardEvent.shiftKey) {
  343. this.closeCalciteDropdown();
  344. }
  345. else if (isFirstItem && keyboardEvent.shiftKey) {
  346. this.closeCalciteDropdown();
  347. }
  348. else if (keyboardEvent.shiftKey) {
  349. this.focusPrevItem(itemToFocus);
  350. }
  351. else {
  352. this.focusNextItem(itemToFocus);
  353. }
  354. break;
  355. case "ArrowDown":
  356. this.focusNextItem(itemToFocus);
  357. break;
  358. case "ArrowUp":
  359. this.focusPrevItem(itemToFocus);
  360. break;
  361. case "Home":
  362. this.focusFirstItem();
  363. break;
  364. case "End":
  365. this.focusLastItem();
  366. break;
  367. }
  368. event.stopPropagation();
  369. }
  370. handleItemSelect(event) {
  371. this.updateSelectedItems();
  372. event.stopPropagation();
  373. this.calciteDropdownSelect.emit({
  374. item: event.detail.requestedDropdownItem
  375. });
  376. if (!this.disableCloseOnSelect ||
  377. event.detail.requestedDropdownGroup.selectionMode === "none") {
  378. this.closeCalciteDropdown();
  379. }
  380. event.stopPropagation();
  381. }
  382. onBeforeOpen() {
  383. this.calciteDropdownBeforeOpen.emit();
  384. }
  385. onOpen() {
  386. this.calciteDropdownOpen.emit();
  387. }
  388. onBeforeClose() {
  389. this.calciteDropdownBeforeClose.emit();
  390. }
  391. onClose() {
  392. this.calciteDropdownClose.emit();
  393. }
  394. updateSelectedItems() {
  395. this.selectedItems = this.items.filter((item) => item.selected);
  396. }
  397. getMaxScrollerHeight() {
  398. const { maxItems, items } = this;
  399. let itemsToProcess = 0;
  400. let maxScrollerHeight = 0;
  401. let groupHeaderHeight;
  402. this.groups.forEach((group) => {
  403. if (maxItems > 0 && itemsToProcess < maxItems) {
  404. Array.from(group.children).forEach((item, index) => {
  405. if (index === 0) {
  406. if (isNaN(groupHeaderHeight)) {
  407. groupHeaderHeight = item.offsetTop;
  408. }
  409. maxScrollerHeight += groupHeaderHeight;
  410. }
  411. if (itemsToProcess < maxItems) {
  412. maxScrollerHeight += item.offsetHeight;
  413. itemsToProcess += 1;
  414. }
  415. });
  416. }
  417. });
  418. return items.length > maxItems ? maxScrollerHeight : 0;
  419. }
  420. closeCalciteDropdown(focusTrigger = true) {
  421. this.open = false;
  422. if (focusTrigger) {
  423. focusElement(this.triggers[0]);
  424. }
  425. }
  426. focusFirstItem() {
  427. const firstItem = this.items[0];
  428. this.getFocusableElement(firstItem);
  429. }
  430. focusLastItem() {
  431. const lastItem = this.items[this.items.length - 1];
  432. this.getFocusableElement(lastItem);
  433. }
  434. focusNextItem(el) {
  435. const index = this.itemIndex(el);
  436. const nextItem = this.items[index + 1] || this.items[0];
  437. this.getFocusableElement(nextItem);
  438. }
  439. focusPrevItem(el) {
  440. const index = this.itemIndex(el);
  441. const prevItem = this.items[index - 1] || this.items[this.items.length - 1];
  442. this.getFocusableElement(prevItem);
  443. }
  444. itemIndex(el) {
  445. return this.items.indexOf(el);
  446. }
  447. getFocusableElement(item) {
  448. if (!item) {
  449. return;
  450. }
  451. const target = item.attributes.isLink
  452. ? item.shadowRoot.querySelector("a")
  453. : item;
  454. focusElement(target);
  455. }
  456. get el() { return this; }
  457. static get watchers() { return {
  458. "active": ["activeHandler"],
  459. "open": ["openHandler"],
  460. "disabled": ["handleDisabledChange"],
  461. "flipPlacements": ["flipPlacementsHandler"],
  462. "maxItems": ["maxItemsHandler"],
  463. "overlayPositioning": ["overlayPositioningHandler"],
  464. "placement": ["placementHandler"]
  465. }; }
  466. static get style() { return dropdownCss; }
  467. }, [1, "calcite-dropdown", {
  468. "active": [1540],
  469. "open": [1540],
  470. "disableCloseOnSelect": [516, "disable-close-on-select"],
  471. "disabled": [516],
  472. "flipPlacements": [16],
  473. "maxItems": [514, "max-items"],
  474. "overlayPositioning": [513, "overlay-positioning"],
  475. "placement": [513],
  476. "scale": [513],
  477. "selectedItems": [1040],
  478. "type": [513],
  479. "width": [513],
  480. "reposition": [64]
  481. }, [[9, "pointerdown", "closeCalciteDropdownOnClick"], [0, "calciteInternalDropdownCloseRequest", "closeCalciteDropdownOnEvent"], [8, "calciteDropdownOpen", "closeCalciteDropdownOnOpenEvent"], [1, "pointerenter", "mouseEnterHandler"], [1, "pointerleave", "mouseLeaveHandler"], [0, "calciteInternalDropdownItemKeyEvent", "calciteInternalDropdownItemKeyEvent"], [0, "calciteInternalDropdownItemSelect", "handleItemSelect"]]]);
  482. function defineCustomElement() {
  483. if (typeof customElements === "undefined") {
  484. return;
  485. }
  486. const components = ["calcite-dropdown"];
  487. components.forEach(tagName => { switch (tagName) {
  488. case "calcite-dropdown":
  489. if (!customElements.get(tagName)) {
  490. customElements.define(tagName, Dropdown);
  491. }
  492. break;
  493. } });
  494. }
  495. defineCustomElement();
  496. export { Dropdown as D, defineCustomElement as d };