Skip to content

Commit 343b184

Browse files
authored
Merge pull request #21035 from itisAliRH/add-history-list-keyboard-navigation
[25.1] Add Keyboard Navigation to History Lists
2 parents eaf525a + 499595d commit 343b184

File tree

8 files changed

+173
-41
lines changed

8 files changed

+173
-41
lines changed

client/src/components/Common/GCard.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ interface Props {
176176
* @default "Last updated"
177177
*/
178178
updateTimeTitle?: string;
179+
180+
/** Whether this card is highlighted (for example, as a range selection anchor)
181+
* @default false
182+
*/
183+
highlighted?: boolean;
179184
}
180185
181186
const props = withDefaults(defineProps<Props>(), {
@@ -207,6 +212,7 @@ const props = withDefaults(defineProps<Props>(), {
207212
updateTime: "",
208213
updateTimeIcon: () => faEdit,
209214
updateTimeTitle: "Last updated",
215+
highlighted: false,
210216
});
211217
212218
/**
@@ -283,8 +289,10 @@ const allowedTitleLines = computed(() => props.titleNLines);
283289
284290
function onKeyDown(event: KeyboardEvent) {
285291
if ((props.clickable && event.key === "Enter") || event.key === " ") {
292+
event.stopPropagation();
286293
emit("click", event);
287294
} else if (props.clickable) {
295+
event.stopPropagation();
288296
emit("keydown", event);
289297
}
290298
}
@@ -310,7 +318,7 @@ function onKeyDown(event: KeyboardEvent) {
310318
<div
311319
:id="`g-card-content-${props.id}`"
312320
class="g-card-content d-flex flex-column justify-content-between h-100 p-2"
313-
:class="contentClass">
321+
:class="[{ 'g-card-highlighted': props.highlighted }, contentClass]">
314322
<slot>
315323
<div class="d-flex flex-column flex-gapy-1">
316324
<div
@@ -708,6 +716,10 @@ function onKeyDown(event: KeyboardEvent) {
708716
border-left: 0.25rem solid $brand-primary;
709717
}
710718
719+
&.g-card-highlighted .g-card-content {
720+
box-shadow: 0 0 0 0.2rem transparentize($brand-primary, 0.75);
721+
}
722+
711723
&.g-card-clickable {
712724
cursor: pointer;
713725

client/src/components/History/HistoryCard.vue

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,20 @@ interface Props {
9393
* @default false
9494
*/
9595
sharedView?: boolean;
96+
97+
/**
98+
* Whether the card is clickable for keyboard navigation
99+
* @type {boolean}
100+
* @default false
101+
*/
102+
clickable?: boolean;
103+
104+
/**
105+
* Whether this card is highlighted (for example, as a range selection anchor)
106+
* @type {boolean}
107+
* @default false
108+
*/
109+
highlighted?: boolean;
96110
}
97111
98112
const props = withDefaults(defineProps<Props>(), {
@@ -102,6 +116,8 @@ const props = withDefaults(defineProps<Props>(), {
102116
selectable: false,
103117
selected: false,
104118
sharedView: false,
119+
clickable: false,
120+
highlighted: false,
105121
});
106122
107123
const router = useRouter();
@@ -144,6 +160,18 @@ const emit = defineEmits<{
144160
* @event updateFilter
145161
*/
146162
(e: "updateFilter", key: string, value: any): void;
163+
164+
/**
165+
* Emitted when a keyboard event occurs on the history card
166+
* @event on-key-down
167+
*/
168+
(e: "on-key-down", history: AnyHistoryEntry, event: KeyboardEvent): void;
169+
170+
/**
171+
* Emitted when the history card is clicked
172+
* @event on-history-card-click
173+
*/
174+
(e: "on-history-card-click", history: AnyHistoryEntry, event: Event): void;
147175
}>();
148176
149177
/**
@@ -203,12 +231,25 @@ async function onTagsUpdate(historyId: string, tags: string[]) {
203231
await historyStore.updateHistory(historyId, { tags: tags });
204232
emit("refreshList", true, true);
205233
}
234+
235+
function onClick(event: Event) {
236+
if (props.clickable) {
237+
emit("on-history-card-click", props.history, event);
238+
}
239+
}
240+
241+
function onKeyDown(event: KeyboardEvent) {
242+
if (props.clickable) {
243+
emit("on-key-down", props.history, event);
244+
}
245+
}
206246
</script>
207247

208248
<template>
209249
<GCard
210250
:id="`history-${history.id}`"
211251
:key="history.id"
252+
class="history-card"
212253
:title="historyCardTitle"
213254
:title-badges="historyCardTitleBadges"
214255
:title-n-lines="2"
@@ -226,11 +267,15 @@ async function onTagsUpdate(historyId: string, tags: string[]) {
226267
:tags-editable="userOwnsHistory(currentUser, history)"
227268
:max-visible-tags="props.gridView ? 2 : 8"
228269
:update-time="history.update_time"
270+
:clickable="props.clickable"
271+
:highlighted="props.highlighted"
229272
@titleClick="onTitleClick"
230273
@rename="() => router.push(`/histories/rename?id=${history.id}`)"
231274
@select="isMyHistory(history) && emit('select', history)"
232275
@tagsUpdate="(tags) => onTagsUpdate(history.id, tags)"
233-
@tagClick="(tag) => emit('tagClick', tag)">
276+
@tagClick="(tag) => emit('tagClick', tag)"
277+
@click="onClick"
278+
@keydown="onKeyDown">
234279
<template v-if="props.archivedView && isArchivedHistory(history)" v-slot:titleActions>
235280
<ExportRecordDOILink :export-record-uri="history.export_record_data?.target_uri" />
236281
</template>

client/src/components/History/HistoryCardList.vue

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
* @tagClick="onTagClick" />
2424
*/
2525
26+
import type { Ref } from "vue";
27+
2628
import type { AnyHistoryEntry, MyHistory } from "@/api/histories";
2729
import { isMyHistory } from "@/api/histories";
2830
@@ -76,13 +78,36 @@ interface Props {
7678
* @default []
7779
*/
7880
selectedHistoryIds?: { id: string }[];
81+
82+
/**
83+
* Whether cards are clickable for navigation
84+
* @type {boolean}
85+
* @default false
86+
*/
87+
clickable?: boolean;
88+
89+
/**
90+
* Item refs for keyboard navigation
91+
* @type {Record<string, Ref<InstanceType<typeof HistoryCard> | null>>}
92+
* @default {}
93+
*/
94+
itemRefs?: Record<string, Ref<InstanceType<typeof HistoryCard> | null>>;
95+
96+
/**
97+
* Range select anchor for keyboard navigation
98+
* @type {AnyHistoryEntry | undefined}
99+
*/
100+
rangeSelectAnchor?: AnyHistoryEntry;
79101
}
80102
81103
const props = withDefaults(defineProps<Props>(), {
82104
gridView: false,
83105
publishedView: false,
84106
selectable: false,
85107
selectedHistoryIds: () => [],
108+
clickable: false,
109+
itemRefs: () => ({}),
110+
rangeSelectAnchor: undefined,
86111
});
87112
88113
/**
@@ -112,29 +137,50 @@ const emit = defineEmits<{
112137
* @event updateFilter
113138
*/
114139
(e: "updateFilter", key: string, value: any): void;
140+
141+
/**
142+
* Emitted when a keyboard event occurs on a history card
143+
* @event on-key-down
144+
*/
145+
(e: "on-key-down", history: AnyHistoryEntry, event: KeyboardEvent): void;
146+
147+
/**
148+
* Emitted when a history card is clicked
149+
* @event on-history-card-click
150+
*/
151+
(e: "on-history-card-click", history: AnyHistoryEntry, event: Event): void;
115152
}>();
116153
</script>
117154

118155
<template>
119-
<div class="history-card-list d-flex flex-wrap overflow-auto">
156+
<div class="history-card-list d-flex flex-wrap overflow-auto pt-1">
120157
<HistoryCard
121158
v-for="history in props.histories"
159+
:ref="props.itemRefs[history.id]"
122160
:key="history.id"
161+
tabindex="0"
123162
:history="history"
124163
:grid-view="props.gridView"
125164
:shared-view="props.sharedView"
126165
:published-view="props.publishedView"
127166
:archived-view="props.archivedView"
128167
:selectable="props.selectable"
129168
:selected="props.selectedHistoryIds.some((selected) => selected.id === history.id)"
169+
:clickable="props.clickable"
170+
:highlighted="props.rangeSelectAnchor?.id === history.id"
171+
class="history-card-in-list"
130172
@select="isMyHistory(history) && emit('select', history)"
131173
@tagClick="(...args) => emit('tagClick', ...args)"
132174
@refreshList="(...args) => emit('refreshList', ...args)"
133-
@updateFilter="(...args) => emit('updateFilter', ...args)" />
175+
@updateFilter="(...args) => emit('updateFilter', ...args)"
176+
@on-key-down="(...args) => emit('on-key-down', ...args)"
177+
@on-history-card-click="(...args) => emit('on-history-card-click', ...args)" />
134178
</div>
135179
</template>
136180

137181
<style lang="scss" scoped>
182+
@import "theme/blue.scss";
183+
138184
.history-card-list {
139185
container: cards-list / inline-size;
140186
}

0 commit comments

Comments
 (0)