tree-item.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  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.82
  5. */
  6. import { Component, Element, Prop, Host, Event, Listen, Watch, h } from "@stencil/core";
  7. import { TreeSelectionMode } from "../tree/interfaces";
  8. import { nodeListToArray, getElementDir, filterDirectChildren, getSlotted, toAriaBoolean } from "../../utils/dom";
  9. import { CSS, SLOTS, ICONS } from "./resources";
  10. import { CSS_UTILITY } from "../../utils/resources";
  11. import { connectConditionalSlotComponent, disconnectConditionalSlotComponent } from "../../utils/conditionalSlot";
  12. /**
  13. * @slot - A slot for adding content to the item.
  14. * @slot children - A slot for adding nested calcite-tree elements.
  15. */
  16. export class TreeItem {
  17. constructor() {
  18. //--------------------------------------------------------------------------
  19. //
  20. // Properties
  21. //
  22. //--------------------------------------------------------------------------
  23. /** Selected state of the item. */
  24. this.selected = false;
  25. /** Expanded state of the item. */
  26. this.expanded = false;
  27. /** @internal Expanded state of the parent. */
  28. this.parentExpanded = false;
  29. /** @internal Level of depth of the item. */
  30. this.depth = -1;
  31. /** @internal Does this tree item have a tree inside it? */
  32. this.hasChildren = null;
  33. this.iconClickHandler = (event) => {
  34. event.stopPropagation();
  35. this.expanded = !this.expanded;
  36. };
  37. this.childrenClickHandler = (event) => event.stopPropagation();
  38. //--------------------------------------------------------------------------
  39. //
  40. // Private Methods
  41. //
  42. //--------------------------------------------------------------------------
  43. this.updateParentIsExpanded = (el, expanded) => {
  44. const items = getSlotted(el, SLOTS.children, {
  45. all: true,
  46. selector: "calcite-tree-item"
  47. });
  48. items.forEach((item) => (item.parentExpanded = expanded));
  49. };
  50. this.updateAncestorTree = () => {
  51. if (this.selected && this.selectionMode === TreeSelectionMode.Ancestors) {
  52. const ancestors = [];
  53. let parent = this.parentTreeItem;
  54. while (parent) {
  55. ancestors.push(parent);
  56. parent = parent.parentElement.closest("calcite-tree-item");
  57. }
  58. ancestors.forEach((item) => (item.indeterminate = true));
  59. return;
  60. }
  61. };
  62. }
  63. expandedHandler(newValue) {
  64. this.updateParentIsExpanded(this.el, newValue);
  65. }
  66. getselectionMode() {
  67. this.isSelectionMultiLike =
  68. this.selectionMode === TreeSelectionMode.Multi ||
  69. this.selectionMode === TreeSelectionMode.MultiChildren;
  70. }
  71. //--------------------------------------------------------------------------
  72. //
  73. // Lifecycle
  74. //
  75. //--------------------------------------------------------------------------
  76. connectedCallback() {
  77. this.parentTreeItem = this.el.parentElement.closest("calcite-tree-item");
  78. if (this.parentTreeItem) {
  79. const { expanded } = this.parentTreeItem;
  80. this.updateParentIsExpanded(this.parentTreeItem, expanded);
  81. }
  82. connectConditionalSlotComponent(this);
  83. }
  84. disconnectedCallback() {
  85. disconnectConditionalSlotComponent(this);
  86. }
  87. componentWillRender() {
  88. this.hasChildren = !!this.el.querySelector("calcite-tree");
  89. this.depth = 0;
  90. let parentTree = this.el.closest("calcite-tree");
  91. if (!parentTree) {
  92. return;
  93. }
  94. this.selectionMode = parentTree.selectionMode;
  95. this.scale = parentTree.scale || "m";
  96. this.lines = parentTree.lines;
  97. let nextParentTree;
  98. while (parentTree) {
  99. nextParentTree = parentTree.parentElement.closest("calcite-tree");
  100. if (nextParentTree === parentTree) {
  101. break;
  102. }
  103. else {
  104. parentTree = nextParentTree;
  105. this.depth = this.depth + 1;
  106. }
  107. }
  108. }
  109. componentDidLoad() {
  110. this.updateAncestorTree();
  111. }
  112. render() {
  113. const rtl = getElementDir(this.el) === "rtl";
  114. const showBulletPoint = this.selectionMode === TreeSelectionMode.Single ||
  115. this.selectionMode === TreeSelectionMode.Children;
  116. const showCheckmark = this.selectionMode === TreeSelectionMode.Multi ||
  117. this.selectionMode === TreeSelectionMode.MultiChildren;
  118. const chevron = this.hasChildren ? (h("calcite-icon", { class: {
  119. [CSS.chevron]: true,
  120. [CSS_UTILITY.rtl]: rtl
  121. }, "data-test-id": "icon", icon: ICONS.chevronRight, onClick: this.iconClickHandler, scale: "s" })) : null;
  122. const defaultSlotNode = h("slot", { key: "default-slot" });
  123. const checkbox = this.selectionMode === TreeSelectionMode.Ancestors ? (h("label", { class: CSS.checkboxLabel, key: "checkbox-label" },
  124. h("calcite-checkbox", { checked: this.selected, class: CSS.checkbox, "data-test-id": "checkbox", indeterminate: this.hasChildren && this.indeterminate, scale: this.scale, tabIndex: -1 }),
  125. defaultSlotNode)) : null;
  126. const selectedIcon = showBulletPoint
  127. ? ICONS.bulletPoint
  128. : showCheckmark
  129. ? ICONS.checkmark
  130. : null;
  131. const bulletOrCheckIcon = selectedIcon ? (h("calcite-icon", { class: {
  132. [CSS.bulletPointIcon]: selectedIcon === ICONS.bulletPoint,
  133. [CSS.checkmarkIcon]: selectedIcon === ICONS.checkmark,
  134. [CSS_UTILITY.rtl]: rtl
  135. }, icon: selectedIcon, scale: "s" })) : null;
  136. const hidden = !(this.parentExpanded || this.depth === 1);
  137. return (h(Host, { "aria-expanded": this.hasChildren ? toAriaBoolean(this.expanded) : undefined, "aria-hidden": toAriaBoolean(hidden), "aria-selected": this.selected ? "true" : showCheckmark ? "false" : undefined, "calcite-hydrated-hidden": hidden, role: "treeitem", tabindex: this.parentExpanded || this.depth === 1 ? "0" : "-1" },
  138. h("div", { class: {
  139. [CSS.nodeContainer]: true,
  140. [CSS_UTILITY.rtl]: rtl
  141. }, "data-selection-mode": this.selectionMode, ref: (el) => (this.defaultSlotWrapper = el) },
  142. chevron,
  143. bulletOrCheckIcon,
  144. checkbox ? checkbox : defaultSlotNode),
  145. h("div", { class: {
  146. [CSS.childrenContainer]: true,
  147. [CSS_UTILITY.rtl]: rtl
  148. }, "data-test-id": "calcite-tree-children", onClick: this.childrenClickHandler, ref: (el) => (this.childrenSlotWrapper = el), role: this.hasChildren ? "group" : undefined },
  149. h("slot", { name: SLOTS.children }))));
  150. }
  151. //--------------------------------------------------------------------------
  152. //
  153. // Event Listeners
  154. //
  155. //--------------------------------------------------------------------------
  156. onClick(e) {
  157. // Solve for if the item is clicked somewhere outside the slotted anchor.
  158. // Anchor is triggered anywhere you click
  159. const [link] = filterDirectChildren(this.el, "a");
  160. if (link && e.composedPath()[0].tagName.toLowerCase() !== "a") {
  161. const target = link.target === "" ? "_self" : link.target;
  162. window.open(link.href, target);
  163. }
  164. this.calciteTreeItemSelect.emit({
  165. modifyCurrentSelection: this.selectionMode === TreeSelectionMode.Ancestors || this.isSelectionMultiLike,
  166. forceToggle: false
  167. });
  168. }
  169. keyDownHandler(e) {
  170. let root;
  171. switch (e.key) {
  172. case " ":
  173. this.calciteTreeItemSelect.emit({
  174. modifyCurrentSelection: this.isSelectionMultiLike,
  175. forceToggle: false
  176. });
  177. e.preventDefault();
  178. break;
  179. case "Enter":
  180. // activates a node, i.e., performs its default action. For parent nodes, one possible default action is to open or close the node. In single-select trees where selection does not follow focus (see note below), the default action is typically to select the focused node.
  181. const link = nodeListToArray(this.el.children).find((e) => e.matches("a"));
  182. if (link) {
  183. link.click();
  184. this.selected = true;
  185. }
  186. else {
  187. this.calciteTreeItemSelect.emit({
  188. modifyCurrentSelection: this.isSelectionMultiLike,
  189. forceToggle: false
  190. });
  191. }
  192. e.preventDefault();
  193. break;
  194. case "Home":
  195. root = this.el.closest("calcite-tree:not([child])");
  196. const firstNode = root.querySelector("calcite-tree-item");
  197. firstNode.focus();
  198. break;
  199. case "End":
  200. root = this.el.closest("calcite-tree:not([child])");
  201. let currentNode = root.children[root.children.length - 1]; // last child
  202. let currentTree = nodeListToArray(currentNode.children).find((e) => e.matches("calcite-tree"));
  203. while (currentTree) {
  204. currentNode = currentTree.children[root.children.length - 1];
  205. currentTree = nodeListToArray(currentNode.children).find((e) => e.matches("calcite-tree"));
  206. }
  207. currentNode.focus();
  208. break;
  209. }
  210. }
  211. static get is() { return "calcite-tree-item"; }
  212. static get encapsulation() { return "shadow"; }
  213. static get originalStyleUrls() { return {
  214. "$": ["tree-item.scss"]
  215. }; }
  216. static get styleUrls() { return {
  217. "$": ["tree-item.css"]
  218. }; }
  219. static get properties() { return {
  220. "selected": {
  221. "type": "boolean",
  222. "mutable": true,
  223. "complexType": {
  224. "original": "boolean",
  225. "resolved": "boolean",
  226. "references": {}
  227. },
  228. "required": false,
  229. "optional": false,
  230. "docs": {
  231. "tags": [],
  232. "text": "Selected state of the item."
  233. },
  234. "attribute": "selected",
  235. "reflect": true,
  236. "defaultValue": "false"
  237. },
  238. "expanded": {
  239. "type": "boolean",
  240. "mutable": true,
  241. "complexType": {
  242. "original": "boolean",
  243. "resolved": "boolean",
  244. "references": {}
  245. },
  246. "required": false,
  247. "optional": false,
  248. "docs": {
  249. "tags": [],
  250. "text": "Expanded state of the item."
  251. },
  252. "attribute": "expanded",
  253. "reflect": true,
  254. "defaultValue": "false"
  255. },
  256. "parentExpanded": {
  257. "type": "boolean",
  258. "mutable": false,
  259. "complexType": {
  260. "original": "boolean",
  261. "resolved": "boolean",
  262. "references": {}
  263. },
  264. "required": false,
  265. "optional": false,
  266. "docs": {
  267. "tags": [{
  268. "name": "internal",
  269. "text": "Expanded state of the parent."
  270. }],
  271. "text": ""
  272. },
  273. "attribute": "parent-expanded",
  274. "reflect": false,
  275. "defaultValue": "false"
  276. },
  277. "depth": {
  278. "type": "number",
  279. "mutable": true,
  280. "complexType": {
  281. "original": "number",
  282. "resolved": "number",
  283. "references": {}
  284. },
  285. "required": false,
  286. "optional": false,
  287. "docs": {
  288. "tags": [{
  289. "name": "internal",
  290. "text": "Level of depth of the item."
  291. }],
  292. "text": ""
  293. },
  294. "attribute": "depth",
  295. "reflect": true,
  296. "defaultValue": "-1"
  297. },
  298. "hasChildren": {
  299. "type": "boolean",
  300. "mutable": true,
  301. "complexType": {
  302. "original": "boolean",
  303. "resolved": "boolean",
  304. "references": {}
  305. },
  306. "required": false,
  307. "optional": false,
  308. "docs": {
  309. "tags": [{
  310. "name": "internal",
  311. "text": "Does this tree item have a tree inside it?"
  312. }],
  313. "text": ""
  314. },
  315. "attribute": "has-children",
  316. "reflect": true,
  317. "defaultValue": "null"
  318. },
  319. "lines": {
  320. "type": "boolean",
  321. "mutable": true,
  322. "complexType": {
  323. "original": "boolean",
  324. "resolved": "boolean",
  325. "references": {}
  326. },
  327. "required": false,
  328. "optional": false,
  329. "docs": {
  330. "tags": [{
  331. "name": "internal",
  332. "text": "Draws lines (set on parent)."
  333. }],
  334. "text": ""
  335. },
  336. "attribute": "lines",
  337. "reflect": true
  338. },
  339. "inputEnabled": {
  340. "type": "boolean",
  341. "mutable": false,
  342. "complexType": {
  343. "original": "boolean",
  344. "resolved": "boolean",
  345. "references": {}
  346. },
  347. "required": false,
  348. "optional": false,
  349. "docs": {
  350. "tags": [{
  351. "name": "internal",
  352. "text": undefined
  353. }, {
  354. "name": "deprecated",
  355. "text": "Use \"ancestors\" selection-mode on parent for checkbox input."
  356. }],
  357. "text": "Displays checkboxes (set on parent)."
  358. },
  359. "attribute": "input-enabled",
  360. "reflect": false
  361. },
  362. "scale": {
  363. "type": "string",
  364. "mutable": true,
  365. "complexType": {
  366. "original": "Scale",
  367. "resolved": "\"l\" | \"m\" | \"s\"",
  368. "references": {
  369. "Scale": {
  370. "location": "import",
  371. "path": "../interfaces"
  372. }
  373. }
  374. },
  375. "required": false,
  376. "optional": false,
  377. "docs": {
  378. "tags": [{
  379. "name": "internal",
  380. "text": "Scale of the parent tree."
  381. }],
  382. "text": ""
  383. },
  384. "attribute": "scale",
  385. "reflect": true
  386. },
  387. "indeterminate": {
  388. "type": "boolean",
  389. "mutable": false,
  390. "complexType": {
  391. "original": "boolean",
  392. "resolved": "boolean",
  393. "references": {}
  394. },
  395. "required": false,
  396. "optional": false,
  397. "docs": {
  398. "tags": [{
  399. "name": "internal",
  400. "text": "In ancestor selection mode,\nshow as indeterminate when only some children are selected."
  401. }],
  402. "text": ""
  403. },
  404. "attribute": "indeterminate",
  405. "reflect": true
  406. },
  407. "selectionMode": {
  408. "type": "string",
  409. "mutable": true,
  410. "complexType": {
  411. "original": "TreeSelectionMode",
  412. "resolved": "TreeSelectionMode.Ancestors | TreeSelectionMode.Children | TreeSelectionMode.Multi | TreeSelectionMode.MultiChildren | TreeSelectionMode.Single",
  413. "references": {
  414. "TreeSelectionMode": {
  415. "location": "import",
  416. "path": "../tree/interfaces"
  417. }
  418. }
  419. },
  420. "required": false,
  421. "optional": false,
  422. "docs": {
  423. "tags": [{
  424. "name": "internal",
  425. "text": "Tree selection-mode (set on parent)."
  426. }],
  427. "text": ""
  428. },
  429. "attribute": "selection-mode",
  430. "reflect": false
  431. }
  432. }; }
  433. static get events() { return [{
  434. "method": "calciteTreeItemSelect",
  435. "name": "calciteTreeItemSelect",
  436. "bubbles": true,
  437. "cancelable": true,
  438. "composed": true,
  439. "docs": {
  440. "tags": [{
  441. "name": "internal",
  442. "text": undefined
  443. }],
  444. "text": ""
  445. },
  446. "complexType": {
  447. "original": "TreeItemSelectDetail",
  448. "resolved": "TreeItemSelectDetail",
  449. "references": {
  450. "TreeItemSelectDetail": {
  451. "location": "import",
  452. "path": "./interfaces"
  453. }
  454. }
  455. }
  456. }]; }
  457. static get elementRef() { return "el"; }
  458. static get watchers() { return [{
  459. "propName": "expanded",
  460. "methodName": "expandedHandler"
  461. }, {
  462. "propName": "selectionMode",
  463. "methodName": "getselectionMode"
  464. }]; }
  465. static get listeners() { return [{
  466. "name": "click",
  467. "method": "onClick",
  468. "target": undefined,
  469. "capture": false,
  470. "passive": false
  471. }, {
  472. "name": "keydown",
  473. "method": "keyDownHandler",
  474. "target": undefined,
  475. "capture": false,
  476. "passive": false
  477. }]; }
  478. }