Skip to content

Commit 32bd442

Browse files
committed
fix(comments): better comments navigation UX
* Header: "< Comment 3 of 4 >" instead of "Comments ^ 3 / 4 v" * Allow to navigate comments with keyboard shortcuts Signed-off-by: Jonas <jonas@freesources.org> Assisted-by: OpenCode:claude-fable-5.1
1 parent 3b8619a commit 32bd442

3 files changed

Lines changed: 75 additions & 17 deletions

File tree

‎playwright/e2e/comments.spec.ts‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,29 @@ test.describe('toggles annotations', () => {
158158
await expect(editor.commentReferences.first()).toBeVisible()
159159
})
160160
})
161+
162+
test.describe('navigates between comments', () => {
163+
test.use({
164+
fileContent: 'The quick[^comment-1] brown[^comment-2] fox.\n\n'
165+
+ '[^comment-1]:\n'
166+
+ ' - @[jane](mention://user/jane) *(2026-07-16T13:12Z)*\n'
167+
+ ' First comment\n\n'
168+
+ '[^comment-2]:\n'
169+
+ ' - @[bob](mention://user/bob) *(2026-07-16T13:13Z)*\n'
170+
+ ' Second comment\n',
171+
})
172+
173+
test('shows the position and moves with buttons and shortcut', async ({ open, editor }) => {
174+
await open()
175+
await editor.getCommentReference('comment-1').click()
176+
await expect(editor.commentBubble).toContainText('Comment 1 of 2')
177+
await expect(editor.commentBubble).toContainText('First comment')
178+
179+
await editor.commentBubble.getByRole('button', { name: 'Next comment' }).click()
180+
await expect(editor.commentBubble).toContainText('Comment 2 of 2')
181+
await expect(editor.commentBubble).toContainText('Second comment')
182+
183+
await editor.commentBubble.press('ControlOrMeta+Alt+ArrowLeft')
184+
await expect(editor.commentBubble).toContainText('Comment 1 of 2')
185+
})
186+
})

‎src/components/Comment/CommentBubbleView.vue‎

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,30 @@
77
<div
88
:id="`comment-bubble-${referenceId}`"
99
class="comment-bubble"
10-
@keydown.escape.prevent.stop="closeAndRefocus">
10+
@keydown="onKeyDown">
1111
<div class="comment-bubble__header">
12-
<span class="comment-bubble__title">{{ t('text', 'Comments') }}</span>
13-
<div v-if="commentCount > 1" class="comment-bubble__nav">
12+
<div class="comment-bubble__nav">
1413
<NcButton
14+
v-if="commentCount > 1"
1515
variant="tertiary"
1616
size="small"
17-
:title="t('text', 'Previous comment')"
17+
:aria-label="t('text', 'Previous comment')"
18+
:title="previousTitle"
1819
@click="navigate('prev')">
1920
<template #icon>
20-
<ChevronUpIcon :size="16" />
21+
<ChevronLeftIcon :size="16" />
2122
</template>
2223
</NcButton>
23-
<span class="comment-bubble__nav-position">{{ commentPosition }} / {{ commentCount }}</span>
24+
<span class="comment-bubble__title">{{ title }}</span>
2425
<NcButton
26+
v-if="commentCount > 1"
2527
variant="tertiary"
2628
size="small"
27-
:title="t('text', 'Next comment')"
29+
:aria-label="t('text', 'Next comment')"
30+
:title="nextTitle"
2831
@click="navigate('next')">
2932
<template #icon>
30-
<ChevronDownIcon :size="16" />
33+
<ChevronRightIcon :size="16" />
3134
</template>
3235
</NcButton>
3336
</div>
@@ -169,14 +172,15 @@ import NcDateTime from '@nextcloud/vue/components/NcDateTime'
169172
import NcRichContenteditable from '@nextcloud/vue/components/NcRichContenteditable'
170173
import NcTextField from '@nextcloud/vue/components/NcTextField'
171174
import CheckIcon from 'vue-material-design-icons/Check.vue'
172-
import ChevronDownIcon from 'vue-material-design-icons/ChevronDown.vue'
173-
import ChevronUpIcon from 'vue-material-design-icons/ChevronUp.vue'
175+
import ChevronLeftIcon from 'vue-material-design-icons/ChevronLeft.vue'
176+
import ChevronRightIcon from 'vue-material-design-icons/ChevronRight.vue'
174177
import CloseIcon from 'vue-material-design-icons/Close.vue'
175178
import DeleteIcon from 'vue-material-design-icons/Delete.vue'
176179
import PencilIcon from 'vue-material-design-icons/Pencil.vue'
177180
import { useGuestName } from '../../composables/useGuestName.ts'
178181
import { createMarkdownSerializer } from '../../extensions/Markdown.ts'
179182
import { commentDraftPrefix } from '../../plugins/referenceHelpers.ts'
183+
import { MODIFIERS, TRANSLATIONS } from '../Menu/keys.js'
180184
181185
const props = defineProps<{
182186
editor: Editor
@@ -200,6 +204,14 @@ const commentCount = computed(() => commentRefIds.value.length)
200204
const commentPosition = computed(() => commentRefIds.value.indexOf(props.referenceId) + 1)
201205
const commentNode = computed<Node | null>(() => commentNodesMap.value[props.referenceId] ?? null)
202206
207+
const title = computed(() => (commentCount.value > 1
208+
? t('text', 'Comment {position} of {count}', { position: commentPosition.value, count: commentCount.value })
209+
: t('text', 'Comment')))
210+
211+
const shortcutPrefix = `${TRANSLATIONS[MODIFIERS.Mod]} + ${TRANSLATIONS[MODIFIERS.Alt]} + `
212+
const previousTitle = t('text', 'Previous comment ({shortcut})', { shortcut: shortcutPrefix + '←' })
213+
const nextTitle = t('text', 'Next comment ({shortcut})', { shortcut: shortcutPrefix + '→' })
214+
203215
const editingItemIndex = ref<number | null>(null)
204216
const editText = ref('')
205217
@@ -397,6 +409,25 @@ async function submitGuestName() {
397409
function closeAndRefocus() {
398410
props.editor.commands.hideCommentBubble({ refocus: true })
399411
}
412+
413+
function onKeyDown(event: KeyboardEvent) {
414+
if (event.key === 'Escape') {
415+
event.preventDefault()
416+
event.stopPropagation()
417+
closeAndRefocus()
418+
return
419+
}
420+
if (!(event.ctrlKey || event.metaKey) || !event.altKey) {
421+
return
422+
}
423+
if (event.key === 'ArrowLeft') {
424+
event.preventDefault()
425+
navigate('prev')
426+
} else if (event.key === 'ArrowRight') {
427+
event.preventDefault()
428+
navigate('next')
429+
}
430+
}
400431
</script>
401432

402433
<style scoped lang="scss">
@@ -425,19 +456,13 @@ function closeAndRefocus() {
425456
font-weight: bold;
426457
font-size: 0.9em;
427458
color: var(--color-text-maxcontrast);
459+
white-space: nowrap;
428460
}
429461
430462
&__nav {
431463
display: flex;
432464
align-items: center;
433465
gap: var(--default-grid-baseline);
434-
margin-inline-start: auto;
435-
}
436-
437-
&__nav-position {
438-
font-size: 0.8em;
439-
color: var(--color-text-maxcontrast);
440-
white-space: nowrap;
441466
}
442467
443468
&__items {

‎src/extensions/CommentBubble.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,13 @@ const CommentBubble = Extension.create({
6363
}
6464
},
6565

66+
addKeyboardShortcuts() {
67+
return {
68+
'Mod-Alt-ArrowLeft': () => this.editor.commands.navigateCommentBubble('prev'),
69+
'Mod-Alt-ArrowRight': () => this.editor.commands.navigateCommentBubble('next'),
70+
}
71+
},
72+
6673
addProseMirrorPlugins() {
6774
return [commentBubble({ editor: this.editor })]
6875
},

0 commit comments

Comments
 (0)