@esri/calcite-components
Version:
Web Components for Esri's Calcite Design System.
443 lines (442 loc) • 15.2 kB
JavaScript
/*!
* All material copyright ESRI, All Rights Reserved, unless otherwise specified.
* See https://github.com/Esri/calcite-components/blob/master/LICENSE.md for details.
* v1.5.0-next.4
*/
import { h, Host } from "@stencil/core";
import { focusElement, getRootNode, nodeListToArray } from "../../utils/dom";
import { getEnabledSiblingItem } from "./utils";
/**
* @slot - A slot for `calcite-tree-item` elements.
*/
export class Tree {
constructor() {
this.lines = false;
this.child = undefined;
this.scale = "m";
this.selectionMode = "single";
this.selectedItems = [];
}
//--------------------------------------------------------------------------
//
// Lifecycle
//
//--------------------------------------------------------------------------
componentWillRender() {
const parent = this.el.parentElement?.closest("calcite-tree");
this.lines = parent ? parent.lines : this.lines;
this.scale = parent ? parent.scale : this.scale;
this.selectionMode = parent ? parent.selectionMode : this.selectionMode;
this.child = !!parent;
}
render() {
return (h(Host, { "aria-multiselectable": this.child
? undefined
: (this.selectionMode === "multiple" || this.selectionMode === "multichildren").toString(), role: !this.child ? "tree" : undefined, tabIndex: this.getRootTabIndex() }, h("slot", null)));
}
//--------------------------------------------------------------------------
//
// Event Listeners
//
//--------------------------------------------------------------------------
onFocus() {
if (!this.child) {
const focusTarget = this.el.querySelector("calcite-tree-item[selected]:not([disabled])") || this.el.querySelector("calcite-tree-item:not([disabled])");
focusElement(focusTarget);
}
}
onFocusIn(event) {
const focusedFromRootOrOutsideTree = event.relatedTarget === this.el || !this.el.contains(event.relatedTarget);
if (focusedFromRootOrOutsideTree) {
// gives user the ability to tab into external elements (modifying tabindex property will not work in firefox)
this.el.removeAttribute("tabindex");
}
}
onFocusOut(event) {
const willFocusOutsideTree = !this.el.contains(event.relatedTarget);
if (willFocusOutsideTree) {
this.el.tabIndex = this.getRootTabIndex();
}
}
onClick(event) {
const target = event.target;
const childItems = nodeListToArray(target.querySelectorAll("calcite-tree-item"));
if (this.child) {
return;
}
if (!this.child) {
event.preventDefault();
event.stopPropagation();
}
if (this.selectionMode === "ancestors" && !this.child) {
this.updateAncestorTree(event);
return;
}
const isNoneSelectionMode = this.selectionMode === "none";
const shouldSelect = this.selectionMode !== null &&
(!target.hasChildren ||
(target.hasChildren &&
(this.selectionMode === "children" || this.selectionMode === "multichildren")));
const shouldDeselectAllChildren = this.selectionMode === "multichildren" && target.hasChildren;
const shouldModifyToCurrentSelection = !isNoneSelectionMode &&
event.detail.modifyCurrentSelection &&
(this.selectionMode === "multiple" || this.selectionMode === "multichildren");
const shouldClearCurrentSelection = !shouldModifyToCurrentSelection &&
(((this.selectionMode === "single" || this.selectionMode === "multiple") &&
childItems.length <= 0) ||
this.selectionMode === "children" ||
this.selectionMode === "multichildren");
const shouldUpdateExpand = ["children", "multichildren"].includes(this.selectionMode) ||
(["single", "multiple"].includes(this.selectionMode) &&
target.hasChildren &&
!event.detail.forceToggle);
if (!this.child) {
const targetItems = [];
if (shouldSelect) {
targetItems.push(target);
}
if (shouldClearCurrentSelection) {
const selectedItems = nodeListToArray(this.el.querySelectorAll("calcite-tree-item[selected]"));
selectedItems.forEach((treeItem) => {
if (!targetItems.includes(treeItem)) {
treeItem.selected = false;
}
});
}
if (shouldUpdateExpand) {
if (["single", "multiple"].includes(this.selectionMode)) {
target.expanded = !target.expanded;
}
else if (this.selectionMode === "multichildren") {
target.expanded = !target.selected;
}
else if (this.selectionMode === "children") {
target.expanded = target.selected ? !target.expanded : true;
}
}
if (shouldDeselectAllChildren) {
childItems.forEach((item) => {
item.selected = false;
if (item.hasChildren) {
item.expanded = false;
}
});
}
if (shouldModifyToCurrentSelection) {
window.getSelection().removeAllRanges();
}
if ((shouldModifyToCurrentSelection && target.selected) || event.detail.forceToggle) {
targetItems.forEach((treeItem) => {
if (!treeItem.disabled) {
treeItem.selected = false;
}
});
}
else if (!isNoneSelectionMode) {
targetItems.forEach((treeItem) => {
if (!treeItem.disabled) {
treeItem.selected = true;
}
});
}
}
this.selectedItems = isNoneSelectionMode
? [target]
: nodeListToArray(this.el.querySelectorAll("calcite-tree-item")).filter((i) => i.selected);
this.calciteTreeSelect.emit();
event.stopPropagation();
}
keyDownHandler(event) {
const root = this.el.closest("calcite-tree:not([child])");
const target = event.target;
if (!(root === this.el && target.tagName === "CALCITE-TREE-ITEM" && this.el.contains(target))) {
return;
}
if (event.key === "ArrowDown") {
const next = getEnabledSiblingItem(target.nextElementSibling, "down");
if (next) {
next.focus();
event.preventDefault();
}
return;
}
if (event.key === "ArrowUp") {
const previous = getEnabledSiblingItem(target.previousElementSibling, "up");
if (previous) {
previous.focus();
event.preventDefault();
}
}
if (event.key === "ArrowLeft" && !target.disabled) {
// When focus is on an open node, closes the node.
if (target.hasChildren && target.expanded) {
target.expanded = false;
event.preventDefault();
return;
}
// When focus is on a child node that is also either an end node or a closed node, moves focus to its parent node.
const parentItem = target.parentElement.closest("calcite-tree-item");
if (parentItem && (!target.hasChildren || target.expanded === false)) {
parentItem.focus();
event.preventDefault();
return;
}
// When focus is on a root node that is also either an end node or a closed node, does nothing.
return;
}
if (event.key === "ArrowRight" && !target.disabled) {
if (target.hasChildren) {
if (target.expanded && getRootNode(this.el).activeElement === target) {
// When focus is on an open node, moves focus to the first child node.
getEnabledSiblingItem(target.querySelector("calcite-tree-item"), "down")?.focus();
event.preventDefault();
}
else {
// When focus is on a closed node, opens the node; focus does not move.
target.expanded = true;
event.preventDefault();
}
}
return;
}
}
updateAncestorTree(event) {
const item = event.target;
if (item.disabled) {
return;
}
const ancestors = [];
let parent = item.parentElement.closest("calcite-tree-item");
while (parent) {
ancestors.push(parent);
parent = parent.parentElement.closest("calcite-tree-item");
}
const childItems = Array.from(item.querySelectorAll("calcite-tree-item:not([disabled])"));
const childItemsWithNoChildren = childItems.filter((child) => !child.hasChildren);
const childItemsWithChildren = childItems.filter((child) => child.hasChildren);
const futureSelected = item.hasChildren
? !(item.selected || item.indeterminate)
: !item.selected;
childItemsWithNoChildren.forEach((el) => {
el.selected = futureSelected;
el.indeterminate = false;
});
function updateItemState(childItems, item) {
const selected = childItems.filter((child) => child.selected);
const unselected = childItems.filter((child) => !child.selected);
item.selected = selected.length === childItems.length;
item.indeterminate = selected.length > 0 && unselected.length > 0;
}
childItemsWithChildren.forEach((el) => {
const directChildItems = Array.from(el.querySelectorAll(":scope > calcite-tree > calcite-tree-item"));
updateItemState(directChildItems, el);
});
if (item.hasChildren) {
updateItemState(childItems, item);
}
else {
item.selected = futureSelected;
item.indeterminate = false;
}
ancestors.forEach((ancestor) => {
const descendants = nodeListToArray(ancestor.querySelectorAll("calcite-tree-item"));
const activeDescendants = descendants.filter((el) => el.selected);
if (activeDescendants.length === 0) {
ancestor.selected = false;
ancestor.indeterminate = false;
return;
}
const indeterminate = activeDescendants.length < descendants.length;
ancestor.indeterminate = indeterminate;
ancestor.selected = !indeterminate;
});
this.selectedItems = nodeListToArray(this.el.querySelectorAll("calcite-tree-item")).filter((i) => i.selected);
this.calciteTreeSelect.emit();
}
// --------------------------------------------------------------------------
//
// Private Methods
//
//--------------------------------------------------------------------------
getRootTabIndex() {
return !this.child ? 0 : -1;
}
static get is() { return "calcite-tree"; }
static get encapsulation() { return "shadow"; }
static get originalStyleUrls() {
return {
"$": ["tree.scss"]
};
}
static get styleUrls() {
return {
"$": ["tree.css"]
};
}
static get properties() {
return {
"lines": {
"type": "boolean",
"mutable": true,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Displays indentation guide lines."
},
"attribute": "lines",
"reflect": true,
"defaultValue": "false"
},
"child": {
"type": "boolean",
"mutable": true,
"complexType": {
"original": "boolean",
"resolved": "boolean",
"references": {}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "internal",
"text": undefined
}],
"text": ""
},
"attribute": "child",
"reflect": true
},
"scale": {
"type": "string",
"mutable": true,
"complexType": {
"original": "Scale",
"resolved": "\"l\" | \"m\" | \"s\"",
"references": {
"Scale": {
"location": "import",
"path": "../interfaces"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [],
"text": "Specifies the size of the component."
},
"attribute": "scale",
"reflect": true,
"defaultValue": "\"m\""
},
"selectionMode": {
"type": "string",
"mutable": true,
"complexType": {
"original": "SelectionMode",
"resolved": "\"ancestors\" | \"children\" | \"multichildren\" | \"multiple\" | \"none\" | \"single\" | \"single-persist\"",
"references": {
"SelectionMode": {
"location": "import",
"path": "../interfaces"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "default",
"text": "\"single\""
}],
"text": "Specifies the selection mode, where\n`\"ancestors\"` displays with a checkbox and allows any number of selections from corresponding parent and child selections,\n`\"children\"` allows any number of selections from one parent from corresponding parent and child selections,\n`\"multichildren\"` allows any number of selections from corresponding parent and child selections,\n`\"multiple\"` allows any number of selections,\n`\"none\"` allows no selections,\n`\"single\"` allows one selection, and\n`\"single-persist\"` allows and requires one selection."
},
"attribute": "selection-mode",
"reflect": true,
"defaultValue": "\"single\""
},
"selectedItems": {
"type": "unknown",
"mutable": true,
"complexType": {
"original": "HTMLCalciteTreeItemElement[]",
"resolved": "HTMLCalciteTreeItemElement[]",
"references": {
"HTMLCalciteTreeItemElement": {
"location": "global"
}
}
},
"required": false,
"optional": false,
"docs": {
"tags": [{
"name": "readonly",
"text": undefined
}],
"text": "Specifies the component's selected items."
},
"defaultValue": "[]"
}
};
}
static get events() {
return [{
"method": "calciteTreeSelect",
"name": "calciteTreeSelect",
"bubbles": true,
"cancelable": false,
"composed": true,
"docs": {
"tags": [],
"text": "Fires when the user selects/deselects `calcite-tree-items`."
},
"complexType": {
"original": "void",
"resolved": "void",
"references": {}
}
}];
}
static get elementRef() { return "el"; }
static get listeners() {
return [{
"name": "focus",
"method": "onFocus",
"target": undefined,
"capture": false,
"passive": false
}, {
"name": "focusin",
"method": "onFocusIn",
"target": undefined,
"capture": false,
"passive": false
}, {
"name": "focusout",
"method": "onFocusOut",
"target": undefined,
"capture": false,
"passive": false
}, {
"name": "calciteInternalTreeItemSelect",
"method": "onClick",
"target": undefined,
"capture": false,
"passive": false
}, {
"name": "keydown",
"method": "keyDownHandler",
"target": undefined,
"capture": false,
"passive": false
}];
}
}