Skip to content
Open
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
31 changes: 17 additions & 14 deletions assets/js/hooks/combobox.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export default {
this.refs.searchInput.dispatchEvent(new Event("input", {bubbles: true}))
}

this.el.setAttribute('data-prima-ready', 'true')
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
},

setupElements() {
Expand Down Expand Up @@ -108,7 +108,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 All @@ -123,7 +123,7 @@ export default {
options.forEach((option, index) => {
if (!option.id) {
const comboboxId = this.el.id || 'combobox'
option.id = `${comboboxId}-option-${index}`
this.js().setAttribute(option, 'id', `${comboboxId}-option-${index}`)
}
})
},
Expand Down Expand Up @@ -247,12 +247,15 @@ export default {
},

setFocus(el) {
this.refs.optionsContainer?.querySelector(SELECTORS.FOCUSED_OPTION)?.removeAttribute('data-focus')
el.setAttribute('data-focus', 'true')
const focusedOption = this.refs.optionsContainer?.querySelector(SELECTORS.FOCUSED_OPTION)
if (focusedOption) {
this.js().removeAttribute(focusedOption, '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 Down Expand Up @@ -335,9 +338,9 @@ export default {
for (const option of allOptions) {
const value = option.getAttribute('data-value')
if (selectedValues.includes(value)) {
option.setAttribute('data-selected', 'true')
this.js().setAttribute(option, 'data-selected', 'true')
} else {
option.removeAttribute('data-selected')
this.js().removeAttribute(option, 'data-selected')
}
}
},
Expand All @@ -350,7 +353,7 @@ export default {

const pill = this.refs.selectionTemplate.content.cloneNode(true)
const item = pill.querySelector(SELECTORS.SELECTION_ITEM)
item.dataset.value = value
this.js().setAttribute(item, 'data-value', value)
item.innerHTML = item.innerHTML.replaceAll('__VALUE__', displayValue)

this.refs.selectionsContainer.appendChild(pill)
Expand Down Expand Up @@ -520,12 +523,12 @@ export default {

showOption(option) {
option.style.display = 'block'
option.removeAttribute('data-hidden')
this.js().removeAttribute(option, 'data-hidden')
},

hideOption(option) {
option.style.display = 'none'
option.setAttribute('data-hidden', 'true')
this.js().setAttribute(option, 'data-hidden', 'true')
},

positionOptions() {
Expand Down Expand Up @@ -579,7 +582,7 @@ export default {
},

handleShowStart() {
this.refs.searchInput.setAttribute('aria-expanded', 'true')
this.js().setAttribute(this.refs.searchInput, 'aria-expanded', 'true')

// Setup autoUpdate to reposition on scroll/resize
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.optionsWrapper, () => {
Expand All @@ -596,8 +599,8 @@ export default {
if (!this.refs.optionsContainer) return

this.liveSocket.execJS(this.refs.optionsContainer, this.refs.optionsContainer.getAttribute('js-hide'));
this.refs.searchInput.setAttribute('aria-expanded', 'false')
this.refs.searchInput.removeAttribute('aria-activedescendant')
this.js().setAttribute(this.refs.searchInput, 'aria-expanded', 'false')
this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant')

this.refs.optionsContainer.addEventListener('phx:hide-end', () => {
const regularOptions = this.getRegularOptions()
Expand Down
97 changes: 53 additions & 44 deletions assets/js/hooks/dropdown.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default {
this.cleanup()
this.setupElements()
this.setupEventListeners()
this.el.setAttribute('data-prima-ready', 'true')
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
},

setupElements() {
Expand All @@ -60,11 +60,11 @@ export default {

setupEventListeners() {
this.listeners = [
[this.refs.button, 'click', this.handleToggle.bind(this)],
[this.refs.button, 'click', this.toggleMenu.bind(this)],
[this.refs.menu, 'mouseover', this.handleMouseOver.bind(this)],
[this.refs.menu, 'click', this.handleMenuClick.bind(this)],
[this.el, 'keydown', this.handleKeydown.bind(this)],
[this.el, 'prima:close', this.handleClose.bind(this)],
[this.el, 'prima:close', this.hideMenu.bind(this)],
[this.refs.menu, 'phx:show-start', this.handleShowStart.bind(this)],
[this.refs.menu, 'phx:hide-end', this.handleHideEnd.bind(this)]
]
Expand Down Expand Up @@ -209,14 +209,6 @@ export default {
this.setFocus(matchingItems[nextIndex])
},

handleClose() {
this.hideMenu()
},

handleToggle() {
this.toggleMenu()
},

handleMouseOver(e) {
if (e.target.getAttribute('role') === 'menuitem' &&
e.target.getAttribute('aria-disabled') !== 'true') {
Expand All @@ -233,7 +225,7 @@ export default {
},

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

// Setup autoUpdate to reposition on scroll/resize
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.menuWrapper, () => {
Expand All @@ -243,8 +235,8 @@ export default {

handleHideEnd() {
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 @@ -269,82 +261,99 @@ 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 focusedItem = this.el.querySelector(SELECTORS.FOCUSED_MENUITEM)
if (focusedItem) {
this.js().removeAttribute(focusedItem, 'data-focus')
}
},

hideMenu() {
liveSocket.execJS(this.refs.menu, this.refs.menu.getAttribute('js-hide'))
this.refs.menuWrapper.style.display = 'none'
},

showMenu() {
// Wrapper pattern: Show wrapper first (display:block) so Floating UI can measure it,
// then position it, then trigger inner menu transition. This prevents the menu from
// briefly appearing at wrong position before jumping to correct position.
this.refs.menuWrapper.style.display = 'block'
this.positionMenu()
liveSocket.execJS(this.refs.menu, this.refs.menu.getAttribute('js-show'))
},

toggleMenu() {
if (this.isMenuVisible()) {
liveSocket.execJS(this.refs.menu, this.refs.menu.getAttribute('js-hide'))
this.refs.menuWrapper.style.display = 'none'
this.hideMenu()
} else {
// Wrapper pattern: Show wrapper first (display:block) so Floating UI can measure it,
// then position it, then trigger inner menu transition. This prevents the menu from
// briefly appearing at wrong position before jumping to correct position.
this.refs.menuWrapper.style.display = 'block'
this.positionMenu()
liveSocket.execJS(this.refs.menu, this.refs.menu.getAttribute('js-show'))
this.showMenu()
}
},

showMenuAndFocusFirst() {
// Show wrapper and position it
this.refs.menuWrapper.style.display = 'block'
this.positionMenu()

// Use show to display the menu
liveSocket.execJS(this.refs.menu, this.refs.menu.getAttribute('js-show'))
this.showMenu()

// Focus the first enabled item after the menu appears
const items = this.getEnabledMenuItems()
if (items.length > 0) {
this.setFocus(items[0])
}
},

showMenuAndFocusLast() {
// Show wrapper and position it
this.refs.menuWrapper.style.display = 'block'
this.positionMenu()

// Use show to display the menu
liveSocket.execJS(this.refs.menu, this.refs.menu.getAttribute('js-show'))
this.showMenu()

// Focus the last enabled item after the menu appears
const items = this.getEnabledMenuItems()
if (items.length > 0) {
this.setFocus(items[items.length - 1])
}
},

setupAriaRelationships(button, menu) {
button.setAttribute('aria-controls', menu.id)
menu.setAttribute('aria-labelledby', button.id)
const dropdownId = this.el.id
const triggerId = button.id || `${dropdownId}-trigger`
const menuId = menu.id || `${dropdownId}-menu`

if (!button.id) this.js().setAttribute(button, 'id', triggerId)
this.js().setAttribute(button, 'aria-controls', menuId)
if (!menu.id) this.js().setAttribute(menu, 'id', menuId)
this.js().setAttribute(menu, 'aria-labelledby', triggerId)

this.setupMenuitemIds()
this.setupSectionLabels()
},

setupMenuitemIds() {
const dropdownId = this.el.id
const items = this.el.querySelectorAll(SELECTORS.MENUITEM)

items.forEach((item, index) => {
if (!item.id) {
this.js().setAttribute(item, 'id', `${dropdownId}-item-${index}`)
}
})
},

setupSectionLabels() {
const dropdownId = this.el.id
const sections = this.el.querySelectorAll('[role="group"]')

sections.forEach((section) => {
sections.forEach((section, sectionIndex) => {
// Check if the first child is a heading (role="presentation")
const firstChild = section.firstElementChild
if (firstChild && firstChild.getAttribute('role') === 'presentation') {
// Ensure the heading has an ID
if (!firstChild.id) {
this.js().setAttribute(firstChild, 'id', `${dropdownId}-section-${sectionIndex}-heading`)
}
// Link the section to the heading
section.setAttribute('aria-labelledby', firstChild.id)
this.js().setAttribute(section, 'aria-labelledby', firstChild.id)
}
})
},
Expand Down
14 changes: 7 additions & 7 deletions assets/js/hooks/modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default {
this.setupDOMEventListeners()
this.setupPushEventListeners()
this.checkInitialShow()
this.el.setAttribute('data-prima-ready', 'true')
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
},

setupPushEventListeners() {
Expand Down Expand Up @@ -99,7 +99,7 @@ export default {
handleModalOpen() {
this.storeFocusedElement()
this.preventBodyScroll()
this.el.removeAttribute('aria-hidden')
this.js().removeAttribute(this.el, 'aria-hidden')
this.maybeExecJS(this.el, "js-show");
this.maybeExecJS(this.ref("modal-overlay"), "js-show");
if (this.async) {
Expand All @@ -113,7 +113,7 @@ export default {
this.maybeExecJS(this.ref("modal-loader"), "js-hide");
this.maybeExecJS(this.ref("modal-panel"), "js-show");
this.setupAriaRelationships()
this.el.removeAttribute('aria-hidden')
this.js().removeAttribute(this.el, 'aria-hidden')

const panelShowEndHandler = this.handlePanelShowEnd.bind(this)
this.ref("modal-panel").addEventListener("phx:show-end", panelShowEndHandler);
Expand All @@ -132,13 +132,13 @@ export default {
this.maybeExecJS(this.ref("modal-panel"), "js-hide");
this.maybeExecJS(this.ref("modal-loader"), "js-hide");
if (this.async) {
this.ref("modal-panel").dataset.primaDirty = true
this.js().setAttribute(this.ref("modal-panel"), 'data-prima-dirty', 'true')
}
},

handleOverlayHideEnd() {
this.maybeExecJS(this.el, "js-hide");
this.el.setAttribute('aria-hidden', 'true')
this.js().setAttribute(this.el, 'aria-hidden', 'true')
this.restoreFocusedElement()
},

Expand Down Expand Up @@ -181,11 +181,11 @@ export default {
if (titleElement) {
// Generate ID for the title if it doesn't have one
if (!titleElement.id) {
titleElement.id = `${modalId}-title`
this.js().setAttribute(titleElement, 'id', `${modalId}-title`)
}

// Set aria-labelledby on the modal container
this.el.setAttribute('aria-labelledby', titleElement.id)
this.js().setAttribute(this.el, 'aria-labelledby', titleElement.id)
}
},

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
</.dropdown_trigger>
<.dropdown_menu id="dropdown-sections-menu">
<.dropdown_section>
<.dropdown_heading id="dropdown-sections-heading-account">
<.dropdown_heading>
Account
</.dropdown_heading>
<.dropdown_item id="dropdown-sections-item-0">
Expand All @@ -18,7 +18,7 @@
<.dropdown_separator />

<.dropdown_section>
<.dropdown_heading id="dropdown-sections-heading-support">
<.dropdown_heading>
Support
</.dropdown_heading>
<.dropdown_item id="dropdown-sections-item-2">
Expand Down
Loading