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
35 changes: 24 additions & 11 deletions assets/js/hooks/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export default {
this.syncSelection()
this.close()
this.sendQuery('')
this.el.setAttribute('data-prima-ready', 'true')
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
},

reconnected() {
Expand Down Expand Up @@ -112,7 +112,7 @@ export default {
if (this.refs.optionsContainer && this.refs.searchInput) {
const optionsId = this.refs.optionsContainer.getAttribute('id')
if (optionsId) {
this.refs.searchInput.setAttribute('aria-controls', optionsId)
this.js().setAttribute(this.refs.searchInput, 'aria-controls', optionsId)
}
}

Expand Down Expand Up @@ -153,13 +153,23 @@ export default {
this.positionOptions()
if (this.isOpen) this.restoreOptionFocus()
this.syncSelectedAttributes()
this.el.setAttribute('data-prima-ready', 'true')
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
},

restoreOptionFocus() {
const options = this.getVisibleOptions()
const focused = this.getCurrentFocusedOption()
if (options.includes(focused)) {
this.js().setAttribute(this.refs.searchInput, 'aria-activedescendant', focused.id)
return
}

const focusedOption = options.find(option => option.dataset.value === this.focusedOptionBeforeUpdate) || options[0]
if (focusedOption) this.setFocus(focusedOption)
if (focusedOption) {
this.setFocus(focusedOption)
} else {
this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant')
}
},

sendQuery(query) {
Expand Down Expand Up @@ -254,12 +264,13 @@ export default {
},

setFocus(el) {
this.refs.optionsContainer?.querySelector(SELECTORS.FOCUSED_OPTION)?.removeAttribute('data-focus')
el.setAttribute('data-focus', 'true')
const focused = this.getCurrentFocusedOption()
if (focused) this.js().removeAttribute(focused, 'data-focus')
this.js().setAttribute(el, 'data-focus', 'true')

// Update aria-activedescendant to point to the focused option
if (el.id) {
this.refs.searchInput.setAttribute('aria-activedescendant', el.id)
this.js().setAttribute(this.refs.searchInput, 'aria-activedescendant', el.id)
}

el.scrollIntoView({ block: 'nearest', inline: 'nearest' })
Expand All @@ -269,6 +280,8 @@ export default {
const firstOption = this.refs.optionsContainer?.querySelector(SELECTORS.VISIBLE_OPTION)
if (firstOption) {
this.setFocus(firstOption)
} else {
this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant')
}
},

Expand Down Expand Up @@ -566,7 +579,7 @@ export default {
if (!this.refs.optionsContainer || this.isOpen) return

this.isOpen = true
this.refs.searchInput.setAttribute('aria-expanded', 'true')
this.js().setAttribute(this.refs.searchInput, 'aria-expanded', 'true')
// Reset local filtering for a fresh opening.
for (const option of this.getRegularOptions()) {
this.showOption(option)
Expand Down Expand Up @@ -598,17 +611,17 @@ export default {

close() {
clearTimeout(this.searchTimer)
this.refs.searchInput.removeAttribute('aria-activedescendant')
this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant')
if (this.refs.createOption) {
this.hideOption(this.refs.createOption)
this.refs.createOption.textContent = ''
this.refs.createOption.removeAttribute('data-focus')
this.js().removeAttribute(this.refs.createOption, 'data-focus')
}

if (!this.refs.optionsContainer || !this.isOpen) return

this.isOpen = false
this.refs.searchInput.setAttribute('aria-expanded', 'false')
this.js().setAttribute(this.refs.searchInput, 'aria-expanded', 'false')
this.liveSocket.execJS(this.refs.optionsContainer, this.refs.optionsContainer.getAttribute('js-hide'))
},

Expand Down
42 changes: 24 additions & 18 deletions assets/js/hooks/dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,12 @@ const SELECTORS = {
export default {
mounted() {
this.initialize()
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
},

updated() {
this.initialize()
this.setFocus(this.el.querySelector(SELECTORS.FOCUSED_MENUITEM))
},

reconnected() {
Expand All @@ -42,20 +44,19 @@ export default {
this.cleanup()
this.setupElements()
this.setupEventListeners()
this.el.setAttribute('data-prima-ready', 'true')
if (this.isMenuVisible()) this.startAutoUpdate()
},

setupElements() {
const button = this.el.querySelector(SELECTORS.BUTTON)
const menuWrapper = this.el.querySelector(SELECTORS.MENU_WRAPPER)
const menu = this.el.querySelector(SELECTORS.MENU)
const items = this.el.querySelectorAll(SELECTORS.MENUITEM)

const referenceSelector = menuWrapper?.getAttribute('data-reference')
const referenceElement = referenceSelector ? document.querySelector(referenceSelector) : button

this.setupAriaRelationships(button, menu)
this.refs = { button, menuWrapper, menu, items, referenceElement }
this.refs = { button, menuWrapper, menu, referenceElement }
},

setupEventListeners() {
Expand Down Expand Up @@ -85,6 +86,14 @@ export default {
}
},

startAutoUpdate() {
// Setup autoUpdate to reposition on scroll/resize
this.cleanupAutoUpdate()
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.menuWrapper, () => {
this.positionMenu()
})
},

cleanupAutoUpdate() {
if (this.autoUpdateCleanup) {
this.autoUpdateCleanup()
Expand Down Expand Up @@ -242,13 +251,9 @@ export default {
this.refs.menu.style.display = shouldBeOpen ? '' : 'none'
if (!shouldBeOpen) return

this.refs.button.setAttribute('aria-expanded', 'true')
this.js().setAttribute(this.refs.button, 'aria-expanded', 'true')

// Setup autoUpdate to reposition on scroll/resize
this.cleanupAutoUpdate()
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.menuWrapper, () => {
this.positionMenu()
})
this.startAutoUpdate()
},

handleHideEnd() {
Expand All @@ -257,8 +262,8 @@ export default {
if (shouldBeOpen) return

this.clearFocus()
this.refs.menu.removeAttribute('aria-activedescendant')
this.refs.button.setAttribute('aria-expanded', 'false')
this.js().removeAttribute(this.refs.menu, 'aria-activedescendant')
this.js().setAttribute(this.refs.button, 'aria-expanded', 'false')
this.refs.menuWrapper.style.display = 'none'
this.cleanupAutoUpdate()
},
Expand All @@ -283,15 +288,16 @@ export default {
setFocus(el) {
this.clearFocus()
if (el && el.getAttribute('aria-disabled') !== 'true') {
el.setAttribute('data-focus', '')
this.refs.menu.setAttribute('aria-activedescendant', el.id)
this.js().setAttribute(el, 'data-focus', '')
this.js().setAttribute(this.refs.menu, 'aria-activedescendant', el.id)
} else {
this.refs.menu.removeAttribute('aria-activedescendant')
this.js().removeAttribute(this.refs.menu, 'aria-activedescendant')
}
},

clearFocus() {
this.el.querySelector(SELECTORS.FOCUSED_MENUITEM)?.removeAttribute('data-focus')
const focused = this.el.querySelector(SELECTORS.FOCUSED_MENUITEM)
if (focused) this.js().removeAttribute(focused, 'data-focus')
},

hideMenu() {
Expand Down Expand Up @@ -343,8 +349,8 @@ export default {
},

setupAriaRelationships(button, menu) {
button.setAttribute('aria-controls', menu.id)
menu.setAttribute('aria-labelledby', button.id)
this.js().setAttribute(button, 'aria-controls', menu.id)
this.js().setAttribute(menu, 'aria-labelledby', button.id)

this.setupSectionLabels()
},
Expand All @@ -357,7 +363,7 @@ export default {
const firstChild = section.firstElementChild
if (firstChild && firstChild.getAttribute('role') === 'presentation') {
// Link the section to the heading
section.setAttribute('aria-labelledby', firstChild.id)
this.js().setAttribute(section, 'aria-labelledby', firstChild.id)
}
})
},
Expand Down
37 changes: 22 additions & 15 deletions assets/js/hooks/listbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,12 @@ const SELECTORS = {
export default {
mounted() {
this.initialize()
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
},

updated() {
this.initialize()
this.setFocus(this.el.querySelector(SELECTORS.FOCUSED_OPTION))
},

reconnected() {
Expand All @@ -47,7 +49,7 @@ export default {
this.setupEventListeners()
this.syncDisabledState()
this.syncSelectionFromInput()
this.el.setAttribute('data-prima-ready', 'true')
if (this.isListboxVisible()) this.startAutoUpdate()
},

setupElements() {
Expand All @@ -65,8 +67,8 @@ export default {
},

setupAriaRelationships(button, listbox) {
button.setAttribute('aria-controls', listbox.id)
listbox.setAttribute('aria-labelledby', button.id)
this.js().setAttribute(button, 'aria-controls', listbox.id)
this.js().setAttribute(listbox, 'aria-labelledby', button.id)
},

syncSelectionFromInput() {
Expand Down Expand Up @@ -107,6 +109,14 @@ export default {
}
},

startAutoUpdate() {
// Setup autoUpdate to reposition on scroll/resize
this.cleanupAutoUpdate()
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.optionsWrapper, () => {
this.positionListbox()
})
},

cleanupAutoUpdate() {
if (this.autoUpdateCleanup) {
this.autoUpdateCleanup()
Expand Down Expand Up @@ -327,15 +337,16 @@ export default {
setFocus(el) {
this.clearFocus()
if (el && el.getAttribute('aria-disabled') !== 'true') {
el.setAttribute('data-focus', '')
this.refs.listbox.setAttribute('aria-activedescendant', el.id)
this.js().setAttribute(el, 'data-focus', '')
this.js().setAttribute(this.refs.listbox, 'aria-activedescendant', el.id)
} else {
this.refs.listbox.removeAttribute('aria-activedescendant')
this.js().removeAttribute(this.refs.listbox, 'aria-activedescendant')
}
},

clearFocus() {
this.el.querySelector(SELECTORS.FOCUSED_OPTION)?.removeAttribute('data-focus')
const focused = this.el.querySelector(SELECTORS.FOCUSED_OPTION)
if (focused) this.js().removeAttribute(focused, 'data-focus')
},

hideListbox() {
Expand Down Expand Up @@ -381,14 +392,10 @@ export default {
this.refs.listbox.style.display = shouldBeOpen ? '' : 'none'
if (!shouldBeOpen) return

this.refs.button.setAttribute('aria-expanded', 'true')
this.js().setAttribute(this.refs.button, 'aria-expanded', 'true')
this.refs.listbox.focus({ preventScroll: true })

// Setup autoUpdate to reposition on scroll/resize
this.cleanupAutoUpdate()
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.optionsWrapper, () => {
this.positionListbox()
})
this.startAutoUpdate()
},

handleHideEnd() {
Expand All @@ -397,8 +404,8 @@ export default {
if (shouldBeOpen) return

this.clearFocus()
this.refs.listbox.removeAttribute('aria-activedescendant')
this.refs.button.setAttribute('aria-expanded', 'false')
this.js().removeAttribute(this.refs.listbox, 'aria-activedescendant')
this.js().setAttribute(this.refs.button, 'aria-expanded', 'false')
this.refs.optionsWrapper.style.display = 'none'
this.cleanupAutoUpdate()
},
Expand Down
Loading
Loading