Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 47 additions & 11 deletions src/auto-complete-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<HTMLInputElement>('input')
}

set inputElement(input: HTMLInputElement | null) {
this.#inputElement = input
this.#reattachState()
}

connectedCallback(): void {
if (!this.isConnected) return
this.#reattachState()
}

disconnectedCallback(): void {
Expand All @@ -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') || ''
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]')
Expand Down
56 changes: 56 additions & 0 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = `
<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)
})
})

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) {
Expand Down