Skip to content

Commit f1db40e

Browse files
committed
fix: focus article view when item selected via keyboard navigation
Signed-off-by: Wolfgang <github@linux-dude.de>
1 parent 1f89326 commit f1db40e

4 files changed

Lines changed: 69 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ You can also check [on GitHub](https://github.com/nextcloud/news/releases), the
1111
### Changed
1212

1313
### Fixed
14+
- Focus article view when item selected via keyboard navigation
1415

1516
# Releases
1617
## [28.5.1] - 2026-06-04

src/components/ContentTemplate.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
:itemCount="items.length"
3737
:itemIndex="currentIndex + 1"
3838
:fetchKey="fetchKey"
39+
:selectedByKeyboard="selectedByKeyboard"
3940
@prevItem="previousItem"
4041
@nextItem="nextItem"
4142
@showDetails="showItem(false)" />
@@ -74,6 +75,7 @@ import {
7475
type PropType,
7576
7677
computed,
78+
nextTick,
7779
onBeforeMount,
7880
onBeforeUnmount,
7981
onMounted,
@@ -147,6 +149,7 @@ const stopPageDownHotkey = ref(null)
147149
148150
const contentElement = ref()
149151
const itemListElement = ref()
152+
const selectedByKeyboard = ref(false)
150153
151154
const displayMode = computed(() => {
152155
return store.getters.displaymode
@@ -232,8 +235,10 @@ function selectItem(item: FeedItem) {
232235
function previousItem() {
233236
// Jump to the previous item
234237
if (currentIndex.value > 0) {
238+
selectedByKeyboard.value = true
235239
const previousItem = props.items[currentIndex.value - 1]
236240
selectItem(previousItem)
241+
resetSelectedByKeyboard()
237242
}
238243
}
239244
@@ -244,11 +249,22 @@ function previousItem() {
244249
function nextItem() {
245250
// Jump to the first item, if none was selected, otherwise jump to the next item
246251
if (props.items.length > 0 && currentIndex.value < props.items.length - 1) {
252+
selectedByKeyboard.value = true
247253
const nextItem = props.items[currentIndex.value + 1]
248254
selectItem(nextItem)
255+
resetSelectedByKeyboard()
249256
}
250257
}
251258
259+
/**
260+
* reset selected by keyboard flag
261+
*/
262+
function resetSelectedByKeyboard() {
263+
nextTick(() => {
264+
selectedByKeyboard.value = false
265+
})
266+
}
267+
252268
/**
253269
* enable PageUp/Down hotkeys with screen reader mode
254270
*/

src/components/feed-display/FeedItemDisplay.vue

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
<template>
22
<div
3+
ref="displayElement"
34
class="feed-item-display"
45
:class="{ screenreader: screenReaderMode }"
56
:style="screenReaderItemHeight"
7+
:tabindex="screenReaderMode ? undefined : '-1'"
68
v-bind="screenReaderMode ? { 'aria-setsize': itemCount, 'aria-posinset': itemIndex } : {}"
79
@focusin="selectItemOnFocus">
810
<ShareItem v-if="showShareMenu" :itemId="item.id" @close="closeShareMenu()" />
@@ -294,6 +296,15 @@ export default defineComponent({
294296
type: String,
295297
required: true,
296298
},
299+
300+
/**
301+
* Whether the item was selected via keyboard navigation
302+
*/
303+
selectedByKeyboard: {
304+
type: Boolean,
305+
required: false,
306+
default: false,
307+
},
297308
},
298309
299310
emits: {
@@ -425,13 +436,21 @@ export default defineComponent({
425436
},
426437
427438
watch: {
428-
// Focus title link in article to emulate structural heading navigation
429-
// with screen readers
430-
async isSelected(newSelected) {
431-
if (newSelected && this.screenReaderMode) {
432-
await this.$nextTick()
433-
this.$refs.titleLink.focus()
434-
}
439+
// Focus title link in article to emulate structural heading navigation with
440+
// screen readers or focus the article view when selected via keyboard navigation
441+
isSelected: {
442+
async handler(newSelected) {
443+
if (newSelected) {
444+
await this.$nextTick()
445+
if (this.screenReaderMode) {
446+
this.$refs.titleLink?.focus()
447+
} else if (this.selectedByKeyboard) {
448+
this.$refs.displayElement?.focus()
449+
}
450+
}
451+
},
452+
453+
immediate: true,
435454
},
436455
},
437456
@@ -689,6 +708,10 @@ export default defineComponent({
689708
flex-direction: column;
690709
}
691710
711+
.feed-item-display:focus {
712+
outline: none;
713+
}
714+
692715
.feed-item-display.screenreader {
693716
overflow: hidden;
694717
}

tests/javascript/unit/components/feed-display/FeedItemDisplay.spec.ts

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,23 +102,40 @@ describe('FeedItemDisplay.vue', () => {
102102
expect(feed).toEqual(mockFeeds[0])
103103
})
104104

105-
it('should focus on new selected item when using screen reader mode', async () => {
105+
it('should focus title on new selected item when using screen reader mode', async () => {
106106
const el = { focus: vi.fn() }
107107
Object.defineProperty(wrapper.vm.$refs, 'titleLink', { value: el, configurable: true })
108108

109109
vi.spyOn(wrapper.vm, 'screenReaderMode', 'get').mockReturnValue(true)
110-
wrapper.vm.$options.watch.isSelected.call(wrapper.vm, true)
110+
wrapper.vm.$options.watch.isSelected.handler.call(wrapper.vm, true)
111111
await nextTick()
112112

113113
expect(el.focus).toHaveBeenCalled()
114114
})
115115

116-
it('should not focus on new selected item when not using screen reader mode', async () => {
116+
it('should focus article pane when selected using keyboard navigation', async () => {
117+
await wrapper.setProps({
118+
selectedByKeyboard: true,
119+
})
117120
const el = { focus: vi.fn() }
118-
Object.defineProperty(wrapper.vm.$refs, 'titleLink', { value: el, configurable: true })
121+
Object.defineProperty(wrapper.vm.$refs, 'displayElement', { value: el, configurable: true })
122+
123+
vi.spyOn(wrapper.vm, 'screenReaderMode', 'get').mockReturnValue(false)
124+
wrapper.vm.$options.watch.isSelected.handler.call(wrapper.vm, true)
125+
await nextTick()
126+
127+
expect(el.focus).toHaveBeenCalled()
128+
})
129+
130+
it('should not focus article pane when selected without keyboard navigation', async () => {
131+
await wrapper.setProps({
132+
selectedByKeyboard: false,
133+
})
134+
const el = { focus: vi.fn() }
135+
Object.defineProperty(wrapper.vm.$refs, 'displayElement', { value: el, configurable: true })
119136

120137
vi.spyOn(wrapper.vm, 'screenReaderMode', 'get').mockReturnValue(false)
121-
wrapper.vm.$options.watch.isSelected.call(wrapper.vm, true)
138+
wrapper.vm.$options.watch.isSelected.handler.call(wrapper.vm, true)
122139
await nextTick()
123140

124141
expect(el.focus).not.toHaveBeenCalled()

0 commit comments

Comments
 (0)