From f4ce73c6e75b6180ca6546583ff405d27874669f Mon Sep 17 00:00:00 2001
From: Hrusikesh Panda <mrchief_2000@yahoo.com>
Date: Sun, 19 Jan 2020 17:41:14 -0500
Subject: [PATCH 1/2] fix: Backspace doesn't raise onTagRemove event

---
 src/index.js                           | 3 +--
 src/tree-manager/keyboardNavigation.js | 4 +---
 2 files changed, 2 insertions(+), 5 deletions(-)

diff --git a/src/index.js b/src/index.js
index 28a9c279..367d8210 100644
--- a/src/index.js
+++ b/src/index.js
@@ -256,8 +256,7 @@ class DropdownTreeSelect extends Component {
       }
       return
     } else if (e.key === 'Backspace' && tags.length && this.searchInput.value.length === 0) {
-      const lastTag = tags.pop()
-      this.onCheckboxChange(lastTag._id, false)
+      this.onTagRemove(tags[tags.length - 1]._id, true)
     } else {
       return
     }
diff --git a/src/tree-manager/keyboardNavigation.js b/src/tree-manager/keyboardNavigation.js
index a41d9c2d..8120f71c 100644
--- a/src/tree-manager/keyboardNavigation.js
+++ b/src/tree-manager/keyboardNavigation.js
@@ -1,5 +1,4 @@
 import nodeVisitor from './nodeVisitor'
-import { getTagId } from '../tag'
 
 const Keys = {
   Up: 'ArrowUp',
@@ -150,8 +149,7 @@ const getNextFocusAfterTagDelete = (deletedId, prevTags, tags, fallback) => {
   if (index < 0 || !tags.length) return fallback
 
   index = tags.length > index ? index : tags.length - 1
-  const newFocusId = tags[index]._id
-  const focusNode = document.getElementById(getTagId(newFocusId))
+  const focusNode = document.getElementById(tags[index].getDOMId('tag'))
   if (focusNode) {
     return focusNode.firstElementChild || fallback
   }

From a100034f7fef62d9b1163596a57af2627e511d5b Mon Sep 17 00:00:00 2001
From: Hrusikesh Panda <mrchief_2000@yahoo.com>
Date: Sun, 19 Jan 2020 17:48:27 -0500
Subject: [PATCH 2/2] fix: Prefix instance id in case of custom node id

Fixes #315
---
 src/tag/index.js                 |  9 ++----
 src/tags/index.js                |  3 +-
 src/tree-manager/flatten-tree.js | 14 +++-------
 src/tree-manager/node.js         | 48 ++++++++++++++++++++++++++++++++
 src/trigger/index.js             |  3 +-
 5 files changed, 58 insertions(+), 19 deletions(-)
 create mode 100644 src/tree-manager/node.js

diff --git a/src/tag/index.js b/src/tag/index.js
index a0e9ce44..c5093a3e 100644
--- a/src/tag/index.js
+++ b/src/tag/index.js
@@ -3,8 +3,6 @@ import React, { PureComponent } from 'react'
 
 import './index.css'
 
-export const getTagId = id => `${id}_tag`
-
 class Tag extends PureComponent {
   static propTypes = {
     id: PropTypes.string.isRequired,
@@ -37,10 +35,9 @@ class Tag extends PureComponent {
   }
 
   render() {
-    const { id, label, labelRemove = 'Remove', readOnly, disabled } = this.props
-
-    const tagId = getTagId(id)
-    const buttonId = `${id}_button`
+    const { label, labelRemove = 'Remove', readOnly, disabled, getDOMId } = this.props
+    const tagId = getDOMId('tag')
+    const buttonId = getDOMId('button')
     const className = ['tag-remove', readOnly && 'readOnly', disabled && 'disabled'].filter(Boolean).join(' ')
     const isDisabled = readOnly || disabled
 
diff --git a/src/tags/index.js b/src/tags/index.js
index b467e67a..fb960261 100644
--- a/src/tags/index.js
+++ b/src/tags/index.js
@@ -7,7 +7,7 @@ import './index.css'
 
 const getTags = (tags = [], onDelete, readOnly, disabled, labelRemove) =>
   tags.map(tag => {
-    const { _id, label, tagClassName, dataset } = tag
+    const { _id, label, tagClassName, dataset, getDOMId } = tag
     return (
       <li
         className={['tag-item', tagClassName].filter(Boolean).join(' ')}
@@ -21,6 +21,7 @@ const getTags = (tags = [], onDelete, readOnly, disabled, labelRemove) =>
           readOnly={readOnly}
           disabled={disabled}
           labelRemove={labelRemove}
+          getDOMId={getDOMId}
         />
       </li>
     )
diff --git a/src/tree-manager/flatten-tree.js b/src/tree-manager/flatten-tree.js
index b5c563d0..f579ada8 100644
--- a/src/tree-manager/flatten-tree.js
+++ b/src/tree-manager/flatten-tree.js
@@ -1,4 +1,5 @@
 import getPartialState from './getPartialState'
+import Node from './node'
 
 import { isEmpty } from '../utils'
 
@@ -216,16 +217,8 @@ function walkNodes({
   _rv = { list: new Map(), defaultValues: [], singleSelectedNode: null },
 }) {
   const single = simple || radio
-  nodes.forEach((node, i) => {
-    node._depth = depth
-
-    if (parent) {
-      node._id = node.id || `${parent._id}-${i}`
-      node._parent = parent._id
-      parent._children.push(node._id)
-    } else {
-      node._id = node.id || `${rootPrefixId ? `${rootPrefixId}-${i}` : i}`
-    }
+  nodes.forEach((n, i) => {
+    const node = new Node({ rootPrefixId, depth, parent, index: i, ...n })
 
     if (single && node.checked) {
       if (_rv.singleSelectedNode) {
@@ -261,6 +254,7 @@ function walkNodes({
         radio,
         showPartialState,
         hierarchical,
+        rootPrefixId,
         _rv,
       })
 
diff --git a/src/tree-manager/node.js b/src/tree-manager/node.js
new file mode 100644
index 00000000..826ab8c5
--- /dev/null
+++ b/src/tree-manager/node.js
@@ -0,0 +1,48 @@
+/**
+ * Represents a tree node (not DOM node) in the data tree.
+ * It can have the following properties:
+ *
+ * id               {string|optional}           User defined id, comes from `data` passed to the component.
+ * _id              {string}                    Internal id, auto generated if `id` is not defined, otherwise set to `id`.
+ * rootPrefixId     {string}                    The id of the component's instance.
+ * parent           {Node}                      Parent node, if a child.
+ * label            {string|required}           Checkbox label
+ * value            {string|required}           Checkbox value
+ * children         {array<node>|optional}      Array of child nodes
+ * checked          {bool|optional}             Initial state of checkbox. if true, checkbox is selected and corresponding pill is rendered.
+ * disabled         {bool|optional}             Selectable state of checkbox. if true, the checkbox is disabled and the node is not selectable.
+ * expanded         {bool|optional}             If true, the node is expanded (children of children nodes are not expanded by default unless children nodes also have expanded: true).
+ * className        {string|optional}           Additional css class for the node. This is helpful to style the nodes your way
+ * tagClassName     {string|optional}           Css class for the corresponding tag. Use this to add custom style the pill corresponding to the node.
+ * actions          {array<object>|optional}    An array of extra action on the node (such as displaying an info icon or any custom icons/elements)
+ * dataset          {object|optional}           Allows data-* attributes to be set on the node and tag elements
+ * isDefaultValue   {bool|optional}             Indicate if a node is a default value. When true, the dropdown will automatically select the node(s) when there is no other selected node. Can be used on more than one node.
+ *
+ */
+export default class Node {
+  constructor({ depth, rootPrefixId, parent, index, ...dataProps }) {
+    // first copy all props coming from data
+    Object.assign(this, dataProps)
+
+    // then assign basic ones
+    this._depth = depth
+    this.rootPrefixId = rootPrefixId
+
+    if (parent) {
+      this._id = this.id || `${parent._id}-${index}`
+      this._parent = parent._id
+      parent._children.push(this._id)
+    } else {
+      this._id = this.id || `${rootPrefixId ? `${rootPrefixId}-${index}` : index}`
+    }
+  }
+
+  /**
+   * Given an element, generate a DOM Id that's unique across instances
+   */
+  getDOMId = element => {
+    // if user has defined id, then ensure it's unique across instances
+    if (this.id) return `${this.rootPrefixId}_${this.id}_${element}`
+    return `${this._id}_${element}`
+  }
+}
diff --git a/src/trigger/index.js b/src/trigger/index.js
index ddead5c6..2d13deca 100644
--- a/src/trigger/index.js
+++ b/src/trigger/index.js
@@ -2,7 +2,6 @@ import React, { PureComponent } from 'react'
 import PropTypes from 'prop-types'
 
 import { getAriaLabel } from '../a11y'
-import { getTagId } from '../tag'
 
 class Trigger extends PureComponent {
   static propTypes = {
@@ -28,7 +27,7 @@ class Trigger extends PureComponent {
         labelledBy.push(triggerId)
       }
       tags.forEach(t => {
-        labelledBy.push(getTagId(t._id))
+        labelledBy.push(t.getDOMId('tag'))
       })
       labelAttributes = getAriaLabel(texts.label, labelledBy.join(' '))
     }