diff --git a/src/auto-complete-element.ts b/src/auto-complete-element.ts index 0f711f8..2e816fa 100644 --- a/src/auto-complete-element.ts +++ b/src/auto-complete-element.ts @@ -6,17 +6,40 @@ const state = new WeakMap() // eslint-disable-next-line custom-elements/file-name-matches-element export default class AutocompleteElement extends HTMLElement { - connectedCallback(): void { - const listId = this.getAttribute('for') - if (!listId) return + #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 + } - // eslint-disable-next-line custom-elements/no-dom-traversal-in-connectedcallback - const input = this.querySelector('input') - const results = 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') + 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('input') + } + + set inputElement(input: HTMLInputElement | null) { + this.#inputElement = input + this.#reattachState() + } + + connectedCallback(): void { + if (!this.isConnected) return + this.#reattachState() } disconnectedCallback(): void { @@ -27,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') || '' } @@ -66,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 { @@ -75,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/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..1c1d368 100644 --- a/test/test.js +++ b/test/test.js @@ -314,6 +314,62 @@ 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 = ` + + + + + + + ` + }) + + 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) + }) + }) + + describe('redefining elements', () => { + beforeEach(function () { + document.body.innerHTML = ` +
+ + + +
    + +
    +
    + ` + }) + + 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) {