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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ You can also check [on GitHub](https://github.com/nextcloud/news/releases), the
### Changed

### Fixed
- Focus article view when item selected via keyboard navigation

# Releases
## [28.5.1] - 2026-06-04
Expand Down
16 changes: 16 additions & 0 deletions src/components/ContentTemplate.vue
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
:itemCount="items.length"
:itemIndex="currentIndex + 1"
:fetchKey="fetchKey"
:selectedByKeyboard="selectedByKeyboard"
@prevItem="previousItem"
@nextItem="nextItem"
@showDetails="showItem(false)" />
Expand Down Expand Up @@ -74,6 +75,7 @@ import {
type PropType,

computed,
nextTick,
onBeforeMount,
onBeforeUnmount,
onMounted,
Expand Down Expand Up @@ -147,6 +149,7 @@ const stopPageDownHotkey = ref(null)

const contentElement = ref()
const itemListElement = ref()
const selectedByKeyboard = ref(false)

const displayMode = computed(() => {
return store.getters.displaymode
Expand Down Expand Up @@ -232,8 +235,10 @@ function selectItem(item: FeedItem) {
function previousItem() {
// Jump to the previous item
if (currentIndex.value > 0) {
selectedByKeyboard.value = true
const previousItem = props.items[currentIndex.value - 1]
selectItem(previousItem)
resetSelectedByKeyboard()
}
}

Expand All @@ -244,11 +249,22 @@ function previousItem() {
function nextItem() {
// Jump to the first item, if none was selected, otherwise jump to the next item
if (props.items.length > 0 && currentIndex.value < props.items.length - 1) {
selectedByKeyboard.value = true
const nextItem = props.items[currentIndex.value + 1]
selectItem(nextItem)
resetSelectedByKeyboard()
}
}

/**
* reset selected by keyboard flag
*/
function resetSelectedByKeyboard() {
nextTick(() => {
selectedByKeyboard.value = false
})
}

/**
* enable PageUp/Down hotkeys with screen reader mode
*/
Expand Down
40 changes: 34 additions & 6 deletions src/components/feed-display/FeedItemDisplay.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
<template>
<div
ref="displayElement"
class="feed-item-display"
:class="{ screenreader: screenReaderMode }"
:style="screenReaderItemHeight"
:tabindex="screenReaderMode ? undefined : '-1'"
v-bind="screenReaderMode ? { 'aria-setsize': itemCount, 'aria-posinset': itemIndex } : {}"
@focusin="selectItemOnFocus">
<ShareItem v-if="showShareMenu" :itemId="item.id" @close="closeShareMenu()" />
Expand Down Expand Up @@ -294,6 +296,15 @@ export default defineComponent({
type: String,
required: true,
},

/**
* Whether the item was selected via keyboard navigation
*/
selectedByKeyboard: {
type: Boolean,
required: false,
default: false,
},
},

emits: {
Expand Down Expand Up @@ -425,13 +436,26 @@ export default defineComponent({
},

watch: {
// Focus title link in article to emulate structural heading navigation
// with screen readers
async isSelected(newSelected) {
if (newSelected && this.screenReaderMode) {
// Focus title link in article to emulate structural heading navigation with
// screen readers or focus the article view when selected via keyboard navigation
isSelected: {
async handler(newSelected) {
if (!newSelected) {
return
}
const screenReaderMode = this.screenReaderMode
const selectedByKeyboard = this.selectedByKeyboard

await this.$nextTick()
this.$refs.titleLink.focus()
}

if (screenReaderMode) {
this.$refs.titleLink?.focus()
} else if (selectedByKeyboard) {
this.$refs.displayElement?.focus()
}
},

immediate: true,
},
},

Expand Down Expand Up @@ -689,6 +713,10 @@ export default defineComponent({
flex-direction: column;
}

.feed-item-display:focus {
outline: none;
}

.feed-item-display.screenreader {
overflow: hidden;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,23 +102,40 @@ describe('FeedItemDisplay.vue', () => {
expect(feed).toEqual(mockFeeds[0])
})

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

vi.spyOn(wrapper.vm, 'screenReaderMode', 'get').mockReturnValue(true)
wrapper.vm.$options.watch.isSelected.call(wrapper.vm, true)
wrapper.vm.$options.watch.isSelected.handler.call(wrapper.vm, true)
await nextTick()

expect(el.focus).toHaveBeenCalled()
})

it('should not focus on new selected item when not using screen reader mode', async () => {
it('should focus article pane when selected using keyboard navigation', async () => {
await wrapper.setProps({
selectedByKeyboard: true,
})
const el = { focus: vi.fn() }
Object.defineProperty(wrapper.vm.$refs, 'titleLink', { value: el, configurable: true })
Object.defineProperty(wrapper.vm.$refs, 'displayElement', { value: el, configurable: true })

vi.spyOn(wrapper.vm, 'screenReaderMode', 'get').mockReturnValue(false)
wrapper.vm.$options.watch.isSelected.handler.call(wrapper.vm, true)
await nextTick()

expect(el.focus).toHaveBeenCalled()
})

it('should not focus article pane when selected without keyboard navigation', async () => {
await wrapper.setProps({
selectedByKeyboard: false,
})
const el = { focus: vi.fn() }
Object.defineProperty(wrapper.vm.$refs, 'displayElement', { value: el, configurable: true })

vi.spyOn(wrapper.vm, 'screenReaderMode', 'get').mockReturnValue(false)
wrapper.vm.$options.watch.isSelected.call(wrapper.vm, true)
wrapper.vm.$options.watch.isSelected.handler.call(wrapper.vm, true)
await nextTick()

expect(el.focus).not.toHaveBeenCalled()
Expand Down
Loading