From b02a30a4747ce75e72f6a5ceb77a1ee6406a2283 Mon Sep 17 00:00:00 2001
From: augonis <plant3@inbox.lv>
Date: Mon, 17 Oct 2022 16:52:15 +0300
Subject: [PATCH 1/2] Fixes #38

Replaces usages of `document.getElementById` with `container.querySelector`
---
 src/auto-complete-element.ts |  3 ++-
 src/autocomplete.ts          |  4 ++--
 test/test.js                 | 28 ++++++++++++++++++++++++++++
 3 files changed, 32 insertions(+), 3 deletions(-)

diff --git a/src/auto-complete-element.ts b/src/auto-complete-element.ts
index 0f711f8..d410c8e 100644
--- a/src/auto-complete-element.ts
+++ b/src/auto-complete-element.ts
@@ -8,11 +8,12 @@ const state = new WeakMap()
 export default class AutocompleteElement extends HTMLElement {
   connectedCallback(): void {
     const listId = this.getAttribute('for')
+    if (!this.isConnected) return
     if (!listId) return
 
     // eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback
     const input = this.querySelector('input')
-    const results = document.getElementById(listId)
+    const results = (this.getRootNode() as Document).getElementById(listId)
     if (!(input instanceof HTMLInputElement) || !results) return
     const autoselectEnabled = this.getAttribute('data-autoselect') === 'true'
     state.set(this, new Autocomplete(this, input, results, autoselectEnabled))
diff --git a/src/autocomplete.ts b/src/autocomplete.ts
index 6124cb8..ea26328 100644
--- a/src/autocomplete.ts
+++ b/src/autocomplete.ts
@@ -28,9 +28,9 @@ export default class Autocomplete {
     this.input = input
     this.results = results
     this.combobox = new Combobox(input, results)
-    this.feedback = document.getElementById(`${this.results.id}-feedback`)
+    this.feedback = (container.getRootNode() as Document).getElementById(`${this.results.id}-feedback`)
     this.autoselectEnabled = autoselectEnabled
-    this.clearButton = document.getElementById(`${this.input.id || this.input.name}-clear`)
+    this.clearButton = (container.getRootNode() as Document).getElementById(`${this.input.id || this.input.name}-clear`)
 
     // check to see if there are any default options provided
     this.clientOptions = results.querySelectorAll('[role=option]')
diff --git a/test/test.js b/test/test.js
index 94981f0..5f47b3b 100644
--- a/test/test.js
+++ b/test/test.js
@@ -314,6 +314,34 @@ describe('auto-complete element', function () {
       assert.equal(feedback.textContent, '')
     })
   })
+
+  describe('shadowdom', () => {
+    let shadow = null
+    beforeEach(function () {
+      const fixture = document.createElement('div')
+      fixture.id = 'mocha-fixture'
+      document.body.append(fixture)
+      shadow = fixture.attachShadow({mode: 'open'})
+      shadow.innerHTML = `
+          <auto-complete src="/search" for="popup">
+            <input type="text">
+            <ul id="popup"></ul>
+            <div id="popup-feedback"></div>
+          </auto-complete>
+        </div>
+      `
+    })
+
+    it('uses rootNode to find idrefs', async function () {
+      const container = shadow.querySelector('auto-complete')
+      const input = container.querySelector('input')
+      const popup = container.querySelector('#popup')
+
+      triggerInput(input, 'hub')
+      await once(container, 'loadend')
+      assert.equal(5, popup.children.length)
+    })
+  })
 })
 
 function waitForElementToChange(el) {

From f14f04debf89a3dd4f34d1c0f1eb755a7a5d4f2e Mon Sep 17 00:00:00 2001
From: Keith Cirkel <keithamus@users.noreply.github.com>
Date: Thu, 30 Mar 2023 12:11:27 +0100
Subject: [PATCH 2/2] allow imperative defining of associated elements

---
 src/auto-complete-element.ts | 57 +++++++++++++++++++++++++++++-------
 test/test.js                 | 28 ++++++++++++++++++
 2 files changed, 74 insertions(+), 11 deletions(-)

diff --git a/src/auto-complete-element.ts b/src/auto-complete-element.ts
index d410c8e..2e816fa 100644
--- a/src/auto-complete-element.ts
+++ b/src/auto-complete-element.ts
@@ -6,18 +6,40 @@ const state = new WeakMap()
 
 // eslint-disable-next-line custom-elements/file-name-matches-element
 export default class AutocompleteElement extends HTMLElement {
+  #forElement: HTMLElement | null = null
+  get forElement(): HTMLElement | null {
+    if (this.#forElement?.isConnected) {
+      return this.#forElement
+    }
+    const id = this.getAttribute('for')
+    const root = this.getRootNode()
+    if (id && (root instanceof Document || root instanceof ShadowRoot)) {
+      return root.getElementById(id)
+    }
+    return null
+  }
+
+  set forElement(element: HTMLElement | null) {
+    this.#forElement = element
+    this.setAttribute('for', '')
+  }
+
+  #inputElement: HTMLInputElement | null = null
+  get inputElement(): HTMLInputElement | null {
+    if (this.#inputElement?.isConnected) {
+      return this.#inputElement
+    }
+    return this.querySelector<HTMLInputElement>('input')
+  }
+
+  set inputElement(input: HTMLInputElement | null) {
+    this.#inputElement = input
+    this.#reattachState()
+  }
+
   connectedCallback(): void {
-    const listId = this.getAttribute('for')
     if (!this.isConnected) return
-    if (!listId) return
-
-    // eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback
-    const input = this.querySelector('input')
-    const results = (this.getRootNode() as Document).getElementById(listId)
-    if (!(input instanceof HTMLInputElement) || !results) return
-    const autoselectEnabled = this.getAttribute('data-autoselect') === 'true'
-    state.set(this, new Autocomplete(this, input, results, autoselectEnabled))
-    results.setAttribute('role', 'listbox')
+    this.#reattachState()
   }
 
   disconnectedCallback(): void {
@@ -28,6 +50,15 @@ export default class AutocompleteElement extends HTMLElement {
     }
   }
 
+  #reattachState() {
+    state.get(this)?.destroy()
+    const {forElement, inputElement} = this
+    if (!forElement || !inputElement) return
+    const autoselectEnabled = this.getAttribute('data-autoselect') === 'true'
+    state.set(this, new Autocomplete(this, inputElement, forElement, autoselectEnabled))
+    forElement.setAttribute('role', 'listbox')
+  }
+
   get src(): string {
     return this.getAttribute('src') || ''
   }
@@ -67,7 +98,7 @@ export default class AutocompleteElement extends HTMLElement {
   fetchResult = fragment
 
   static get observedAttributes(): string[] {
-    return ['open', 'value']
+    return ['open', 'value', 'for']
   }
 
   attributeChangedCallback(name: string, oldValue: string, newValue: string): void {
@@ -76,6 +107,10 @@ export default class AutocompleteElement extends HTMLElement {
     const autocomplete = state.get(this)
     if (!autocomplete) return
 
+    if (this.forElement !== state.get(this)?.results || this.inputElement !== state.get(this)?.input) {
+      this.#reattachState()
+    }
+
     switch (name) {
       case 'open':
         newValue === null ? autocomplete.close() : autocomplete.open()
diff --git a/test/test.js b/test/test.js
index 5f47b3b..1c1d368 100644
--- a/test/test.js
+++ b/test/test.js
@@ -342,6 +342,34 @@ describe('auto-complete element', function () {
       assert.equal(5, popup.children.length)
     })
   })
+
+  describe('redefining elements', () => {
+    beforeEach(function () {
+      document.body.innerHTML = `
+        <div id="mocha-fixture">
+          <auto-complete src="/search" data-autoselect="true">
+            <input type="text">
+            <input id="second" type="text">
+            <ul></ul>
+            <div id="popup-feedback"></div>
+          </auto-complete>
+        </div>
+      `
+    })
+
+    it('changes where content gets rendered based on properties', async function () {
+      const container = document.querySelector('auto-complete')
+      const input = container.querySelector('input#second')
+      const list = container.querySelector('ul')
+      container.forElement = list
+      container.inputElement = input
+
+      triggerInput(input, 'hub')
+      await once(container, 'loadend')
+
+      assert.equal(5, list.children.length)
+    })
+  })
 })
 
 function waitForElementToChange(el) {
