Skip to content

Commit 110723e

Browse files
authored
Merge pull request #37 from plausible/liveview-native-state
Liveview native state
2 parents f7db164 + b4f80f2 commit 110723e

7 files changed

Lines changed: 89 additions & 69 deletions

File tree

‎assets/js/hooks/combobox.js‎

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export default {
4040
this.syncSelection()
4141
this.close()
4242
this.sendQuery('')
43-
this.el.setAttribute('data-prima-ready', 'true')
43+
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
4444
},
4545

4646
reconnected() {
@@ -112,7 +112,7 @@ export default {
112112
if (this.refs.optionsContainer && this.refs.searchInput) {
113113
const optionsId = this.refs.optionsContainer.getAttribute('id')
114114
if (optionsId) {
115-
this.refs.searchInput.setAttribute('aria-controls', optionsId)
115+
this.js().setAttribute(this.refs.searchInput, 'aria-controls', optionsId)
116116
}
117117
}
118118

@@ -153,13 +153,23 @@ export default {
153153
this.positionOptions()
154154
if (this.isOpen) this.restoreOptionFocus()
155155
this.syncSelectedAttributes()
156-
this.el.setAttribute('data-prima-ready', 'true')
156+
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
157157
},
158158

159159
restoreOptionFocus() {
160160
const options = this.getVisibleOptions()
161+
const focused = this.getCurrentFocusedOption()
162+
if (options.includes(focused)) {
163+
this.js().setAttribute(this.refs.searchInput, 'aria-activedescendant', focused.id)
164+
return
165+
}
166+
161167
const focusedOption = options.find(option => option.dataset.value === this.focusedOptionBeforeUpdate) || options[0]
162-
if (focusedOption) this.setFocus(focusedOption)
168+
if (focusedOption) {
169+
this.setFocus(focusedOption)
170+
} else {
171+
this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant')
172+
}
163173
},
164174

165175
sendQuery(query) {
@@ -254,12 +264,13 @@ export default {
254264
},
255265

256266
setFocus(el) {
257-
this.refs.optionsContainer?.querySelector(SELECTORS.FOCUSED_OPTION)?.removeAttribute('data-focus')
258-
el.setAttribute('data-focus', 'true')
267+
const focused = this.getCurrentFocusedOption()
268+
if (focused) this.js().removeAttribute(focused, 'data-focus')
269+
this.js().setAttribute(el, 'data-focus', 'true')
259270

260271
// Update aria-activedescendant to point to the focused option
261272
if (el.id) {
262-
this.refs.searchInput.setAttribute('aria-activedescendant', el.id)
273+
this.js().setAttribute(this.refs.searchInput, 'aria-activedescendant', el.id)
263274
}
264275

265276
el.scrollIntoView({ block: 'nearest', inline: 'nearest' })
@@ -269,6 +280,8 @@ export default {
269280
const firstOption = this.refs.optionsContainer?.querySelector(SELECTORS.VISIBLE_OPTION)
270281
if (firstOption) {
271282
this.setFocus(firstOption)
283+
} else {
284+
this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant')
272285
}
273286
},
274287

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

568581
this.isOpen = true
569-
this.refs.searchInput.setAttribute('aria-expanded', 'true')
582+
this.js().setAttribute(this.refs.searchInput, 'aria-expanded', 'true')
570583
// Reset local filtering for a fresh opening.
571584
for (const option of this.getRegularOptions()) {
572585
this.showOption(option)
@@ -598,17 +611,17 @@ export default {
598611

599612
close() {
600613
clearTimeout(this.searchTimer)
601-
this.refs.searchInput.removeAttribute('aria-activedescendant')
614+
this.js().removeAttribute(this.refs.searchInput, 'aria-activedescendant')
602615
if (this.refs.createOption) {
603616
this.hideOption(this.refs.createOption)
604617
this.refs.createOption.textContent = ''
605-
this.refs.createOption.removeAttribute('data-focus')
618+
this.js().removeAttribute(this.refs.createOption, 'data-focus')
606619
}
607620

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

610623
this.isOpen = false
611-
this.refs.searchInput.setAttribute('aria-expanded', 'false')
624+
this.js().setAttribute(this.refs.searchInput, 'aria-expanded', 'false')
612625
this.liveSocket.execJS(this.refs.optionsContainer, this.refs.optionsContainer.getAttribute('js-hide'))
613626
},
614627

‎assets/js/hooks/dropdown.js‎

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@ const SELECTORS = {
2424
export default {
2525
mounted() {
2626
this.initialize()
27+
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
2728
},
2829

2930
updated() {
3031
this.initialize()
32+
this.setFocus(this.el.querySelector(SELECTORS.FOCUSED_MENUITEM))
3133
},
3234

3335
reconnected() {
@@ -42,20 +44,19 @@ export default {
4244
this.cleanup()
4345
this.setupElements()
4446
this.setupEventListeners()
45-
this.el.setAttribute('data-prima-ready', 'true')
47+
if (this.isMenuVisible()) this.startAutoUpdate()
4648
},
4749

4850
setupElements() {
4951
const button = this.el.querySelector(SELECTORS.BUTTON)
5052
const menuWrapper = this.el.querySelector(SELECTORS.MENU_WRAPPER)
5153
const menu = this.el.querySelector(SELECTORS.MENU)
52-
const items = this.el.querySelectorAll(SELECTORS.MENUITEM)
5354

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

5758
this.setupAriaRelationships(button, menu)
58-
this.refs = { button, menuWrapper, menu, items, referenceElement }
59+
this.refs = { button, menuWrapper, menu, referenceElement }
5960
},
6061

6162
setupEventListeners() {
@@ -85,6 +86,14 @@ export default {
8586
}
8687
},
8788

89+
startAutoUpdate() {
90+
// Setup autoUpdate to reposition on scroll/resize
91+
this.cleanupAutoUpdate()
92+
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.menuWrapper, () => {
93+
this.positionMenu()
94+
})
95+
},
96+
8897
cleanupAutoUpdate() {
8998
if (this.autoUpdateCleanup) {
9099
this.autoUpdateCleanup()
@@ -242,13 +251,9 @@ export default {
242251
this.refs.menu.style.display = shouldBeOpen ? '' : 'none'
243252
if (!shouldBeOpen) return
244253

245-
this.refs.button.setAttribute('aria-expanded', 'true')
254+
this.js().setAttribute(this.refs.button, 'aria-expanded', 'true')
246255

247-
// Setup autoUpdate to reposition on scroll/resize
248-
this.cleanupAutoUpdate()
249-
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.menuWrapper, () => {
250-
this.positionMenu()
251-
})
256+
this.startAutoUpdate()
252257
},
253258

254259
handleHideEnd() {
@@ -257,8 +262,8 @@ export default {
257262
if (shouldBeOpen) return
258263

259264
this.clearFocus()
260-
this.refs.menu.removeAttribute('aria-activedescendant')
261-
this.refs.button.setAttribute('aria-expanded', 'false')
265+
this.js().removeAttribute(this.refs.menu, 'aria-activedescendant')
266+
this.js().setAttribute(this.refs.button, 'aria-expanded', 'false')
262267
this.refs.menuWrapper.style.display = 'none'
263268
this.cleanupAutoUpdate()
264269
},
@@ -283,15 +288,16 @@ export default {
283288
setFocus(el) {
284289
this.clearFocus()
285290
if (el && el.getAttribute('aria-disabled') !== 'true') {
286-
el.setAttribute('data-focus', '')
287-
this.refs.menu.setAttribute('aria-activedescendant', el.id)
291+
this.js().setAttribute(el, 'data-focus', '')
292+
this.js().setAttribute(this.refs.menu, 'aria-activedescendant', el.id)
288293
} else {
289-
this.refs.menu.removeAttribute('aria-activedescendant')
294+
this.js().removeAttribute(this.refs.menu, 'aria-activedescendant')
290295
}
291296
},
292297

293298
clearFocus() {
294-
this.el.querySelector(SELECTORS.FOCUSED_MENUITEM)?.removeAttribute('data-focus')
299+
const focused = this.el.querySelector(SELECTORS.FOCUSED_MENUITEM)
300+
if (focused) this.js().removeAttribute(focused, 'data-focus')
295301
},
296302

297303
hideMenu() {
@@ -343,8 +349,8 @@ export default {
343349
},
344350

345351
setupAriaRelationships(button, menu) {
346-
button.setAttribute('aria-controls', menu.id)
347-
menu.setAttribute('aria-labelledby', button.id)
352+
this.js().setAttribute(button, 'aria-controls', menu.id)
353+
this.js().setAttribute(menu, 'aria-labelledby', button.id)
348354

349355
this.setupSectionLabels()
350356
},
@@ -357,7 +363,7 @@ export default {
357363
const firstChild = section.firstElementChild
358364
if (firstChild && firstChild.getAttribute('role') === 'presentation') {
359365
// Link the section to the heading
360-
section.setAttribute('aria-labelledby', firstChild.id)
366+
this.js().setAttribute(section, 'aria-labelledby', firstChild.id)
361367
}
362368
})
363369
},

‎assets/js/hooks/listbox.js‎

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,12 @@ const SELECTORS = {
2727
export default {
2828
mounted() {
2929
this.initialize()
30+
this.js().setAttribute(this.el, 'data-prima-ready', 'true')
3031
},
3132

3233
updated() {
3334
this.initialize()
35+
this.setFocus(this.el.querySelector(SELECTORS.FOCUSED_OPTION))
3436
},
3537

3638
reconnected() {
@@ -47,7 +49,7 @@ export default {
4749
this.setupEventListeners()
4850
this.syncDisabledState()
4951
this.syncSelectionFromInput()
50-
this.el.setAttribute('data-prima-ready', 'true')
52+
if (this.isListboxVisible()) this.startAutoUpdate()
5153
},
5254

5355
setupElements() {
@@ -65,8 +67,8 @@ export default {
6567
},
6668

6769
setupAriaRelationships(button, listbox) {
68-
button.setAttribute('aria-controls', listbox.id)
69-
listbox.setAttribute('aria-labelledby', button.id)
70+
this.js().setAttribute(button, 'aria-controls', listbox.id)
71+
this.js().setAttribute(listbox, 'aria-labelledby', button.id)
7072
},
7173

7274
syncSelectionFromInput() {
@@ -107,6 +109,14 @@ export default {
107109
}
108110
},
109111

112+
startAutoUpdate() {
113+
// Setup autoUpdate to reposition on scroll/resize
114+
this.cleanupAutoUpdate()
115+
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.optionsWrapper, () => {
116+
this.positionListbox()
117+
})
118+
},
119+
110120
cleanupAutoUpdate() {
111121
if (this.autoUpdateCleanup) {
112122
this.autoUpdateCleanup()
@@ -327,15 +337,16 @@ export default {
327337
setFocus(el) {
328338
this.clearFocus()
329339
if (el && el.getAttribute('aria-disabled') !== 'true') {
330-
el.setAttribute('data-focus', '')
331-
this.refs.listbox.setAttribute('aria-activedescendant', el.id)
340+
this.js().setAttribute(el, 'data-focus', '')
341+
this.js().setAttribute(this.refs.listbox, 'aria-activedescendant', el.id)
332342
} else {
333-
this.refs.listbox.removeAttribute('aria-activedescendant')
343+
this.js().removeAttribute(this.refs.listbox, 'aria-activedescendant')
334344
}
335345
},
336346

337347
clearFocus() {
338-
this.el.querySelector(SELECTORS.FOCUSED_OPTION)?.removeAttribute('data-focus')
348+
const focused = this.el.querySelector(SELECTORS.FOCUSED_OPTION)
349+
if (focused) this.js().removeAttribute(focused, 'data-focus')
339350
},
340351

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

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

387-
// Setup autoUpdate to reposition on scroll/resize
388-
this.cleanupAutoUpdate()
389-
this.autoUpdateCleanup = autoUpdate(this.refs.referenceElement, this.refs.optionsWrapper, () => {
390-
this.positionListbox()
391-
})
398+
this.startAutoUpdate()
392399
},
393400

394401
handleHideEnd() {
@@ -397,8 +404,8 @@ export default {
397404
if (shouldBeOpen) return
398405

399406
this.clearFocus()
400-
this.refs.listbox.removeAttribute('aria-activedescendant')
401-
this.refs.button.setAttribute('aria-expanded', 'false')
407+
this.js().removeAttribute(this.refs.listbox, 'aria-activedescendant')
408+
this.js().setAttribute(this.refs.button, 'aria-expanded', 'false')
402409
this.refs.optionsWrapper.style.display = 'none'
403410
this.cleanupAutoUpdate()
404411
},

0 commit comments

Comments
 (0)