Skip to content

Commit 1cec5db

Browse files
authored
Merge pull request #5321 from bjester/hotkey-jump-link
Add keyboard navigation links and scrolling for jumping in import modal
2 parents b09a72f + b7565bd commit 1cec5db

9 files changed

Lines changed: 204 additions & 10 deletions

File tree

contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/BrowsingCard.vue

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22

33
<VCard
4+
ref="card"
45
hover
56
@click="handleClick"
67
>
@@ -188,6 +189,12 @@
188189
this.$emit('preview');
189190
}
190191
},
192+
/**
193+
* @public
194+
*/
195+
focus() {
196+
this.$refs.card.$el.focus();
197+
},
191198
},
192199
$trs: {
193200
tagsList: 'Tags: {tags}',

contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ChannelInfoCard.vue

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<template>
22

33
<VCard
4+
ref="card"
45
hover
56
:to="channelRoute"
67
>
@@ -106,6 +107,14 @@
106107
};
107108
},
108109
},
110+
methods: {
111+
/**
112+
* @public
113+
*/
114+
focus() {
115+
this.$refs.card.$el.focus();
116+
},
117+
},
109118
$trs: {
110119
resourceCount: '{count, number} {count, plural, one {resource} other {resources}}',
111120
},

contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ChannelList.vue

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<div v-else>
4545
<ChannelInfoCard
4646
v-for="channel in channels"
47+
:ref="setFirstChannelCardRef"
4748
:key="channel.id"
4849
:channel="channel"
4950
class="mb-3"
@@ -87,6 +88,7 @@
8788
channels: [],
8889
pageCount: 0,
8990
loading: false,
91+
firstChannelCardRef: null,
9092
};
9193
},
9294
computed: {
@@ -131,6 +133,7 @@
131133
...mapActions('importFromChannels', ['loadChannels']),
132134
loadPage() {
133135
this.loading = true;
136+
this.firstChannelCardRef = null;
134137
this.loadChannels({
135138
languages: this.languageFilter,
136139
[this.channelFilter]: true,
@@ -144,6 +147,19 @@
144147
this.loading = false;
145148
});
146149
},
150+
setFirstChannelCardRef(ref) {
151+
if (!this.firstChannelCardRef) {
152+
this.firstChannelCardRef = ref;
153+
}
154+
},
155+
/**
156+
* @public
157+
*/
158+
focus() {
159+
if (this.firstChannelCardRef) {
160+
this.firstChannelCardRef.focus();
161+
}
162+
},
147163
},
148164
$trs: {
149165
channelFilterLabel: 'Channels',

contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/ContentTreeList.vue

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
<VFlex shrink>
3737
<Checkbox
3838
:key="`checkbox-${node.id}`"
39+
:ref="setFirstCardCheckboxRef"
3940
:inputValue="isSelected(node)"
4041
:disabled="ancestorIsSelected"
4142
@input="toggleSelected(node)"
@@ -108,6 +109,7 @@
108109
loading: false,
109110
more: null,
110111
moreLoading: false,
112+
firstCardCheckboxRef: null,
111113
};
112114
},
113115
computed: {
@@ -184,6 +186,7 @@
184186
...mapActions('contentNode', ['loadChildren', 'loadAncestors', 'loadContentNodes']),
185187
loadData() {
186188
this.loading = true;
189+
this.firstCardCheckboxRef = null;
187190
const params = {
188191
complete: true,
189192
};
@@ -201,6 +204,8 @@
201204
this.loadAncestors({ id: this.topicId }),
202205
]).then(() => {
203206
this.loading = false;
207+
// scroll to top via focus
208+
this.$nextTick(() => this.focus());
204209
});
205210
},
206211
/**
@@ -227,6 +232,19 @@
227232
});
228233
}
229234
},
235+
setFirstCardCheckboxRef(ref) {
236+
if (!this.firstCardCheckboxRef) {
237+
this.firstCardCheckboxRef = ref;
238+
}
239+
},
240+
/**
241+
* @public
242+
*/
243+
focus() {
244+
if (this.firstCardCheckboxRef) {
245+
this.firstCardCheckboxRef.focus();
246+
}
247+
},
230248
},
231249
$trs: {
232250
allChannelsLabel: 'Channels',

contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchFilterBar.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22

33
<VContainer
4-
class="pt-3 px-2"
4+
class="pt-2 px-2"
55
fluid
66
>
77
<VChip

contentcuration/contentcuration/frontend/channelEdit/views/ImportFromChannels/SearchOrBrowseWindow.vue

Lines changed: 91 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,26 @@
5858
</VTextField>
5959
</VForm>
6060

61-
<div
62-
v-if="!isBrowsing"
63-
class="my-2 px-2"
64-
>
61+
<div class="my-2">
6562
<ActionLink
66-
class="mb-3"
63+
v-if="!isBrowsing"
6764
:text="$tr('savedSearchesLabel')"
6865
:disabled="!savedSearchesExist"
6966
@click="showSavedSearches = true"
7067
/>
68+
<ActionLink
69+
v-if="shouldShowRecommendations"
70+
:class="{ 'keyboard-visibility': true, 'mx-3': !isBrowsing }"
71+
:text="$tr('jumpToRecommendations')"
72+
:style="keyboardVisibilityStyle"
73+
@click="handleJumpToRecommendations"
74+
/>
7175
</div>
76+
7277
<!-- Search or Topics Browsing -->
7378
<ChannelList
74-
v-if="isBrowsing && !$route.params.channelId"
79+
v-if="isBrowsing && !browseChannelId"
80+
ref="channelList"
7581
@update-language="updateLanguageQuery"
7682
/>
7783
<ContentTreeList
@@ -86,11 +92,20 @@
8692
/>
8793
<SearchResultsList
8894
v-else
95+
ref="searchResultList"
8996
:selected.sync="selected"
9097
@preview="preview($event)"
9198
@change_selected="handleChangeSelected"
9299
@copy_to_clipboard="handleCopyToClipboard"
93100
/>
101+
<div style="text-align: center">
102+
<ActionLink
103+
:text="$tr('jumpToTop')"
104+
class="keyboard-visibility"
105+
:style="keyboardVisibilityStyle"
106+
@click="handleJumpToSearch"
107+
/>
108+
</div>
94109
</KGridItem>
95110

96111
<!-- Recommended resources panel >= 400px -->
@@ -105,16 +120,23 @@
105120
</h3>
106121
<div class="my-3 px-2">
107122
<ActionLink
123+
class="mr-3"
108124
:text="aboutRecommendationsText$()"
109125
@click="handleAboutRecommendations"
110126
/>
127+
<ActionLink
128+
:text="isBrowsing ? $tr('jumpToSearch') : $tr('jumpToSearchResults')"
129+
:style="keyboardVisibilityStyle"
130+
@click="handleJumpToSearch"
131+
/>
111132
</div>
112133

113134
<div class="ml-1">
114135
<KCardGrid layout="1-1-1">
115136
<RecommendedResourceCard
116137
v-for="recommendation in displayedRecommendations"
117138
:key="recommendation.id"
139+
:ref="setFirstRecommendationRef"
118140
:node="recommendation"
119141
@change_selected="handleChangeSelected"
120142
@preview="
@@ -161,6 +183,13 @@
161183
/>
162184
</div>
163185
</div>
186+
<div class="px-2">
187+
<ActionLink
188+
:text="$tr('jumpToTop')"
189+
:style="keyboardVisibilityStyle"
190+
@click="handleJumpToRecommendations"
191+
/>
192+
</div>
164193
</KGridItem>
165194
</KGrid>
166195
<SavedSearchesModal v-model="showSavedSearches" />
@@ -361,6 +390,7 @@
361390
importedNodeIds: [],
362391
rejectedNode: null,
363392
showFeedbackErrorMessage: false,
393+
firstRecommendationRef: null,
364394
};
365395
},
366396
computed: {
@@ -372,6 +402,9 @@
372402
isBrowsing() {
373403
return this.$route.name === RouteNames.IMPORT_FROM_CHANNELS_BROWSE;
374404
},
405+
browseChannelId() {
406+
return this.$route.params.channelId;
407+
},
375408
backToBrowseRoute() {
376409
const query = {
377410
channel_list: this.$route.query.channel_list,
@@ -390,6 +423,11 @@
390423
this.searchTerm.trim() !== this.$route.params.searchTerm
391424
);
392425
},
426+
keyboardVisibilityStyle() {
427+
return {
428+
opacity: this.$inputModality === 'keyboard' ? '1' : '0',
429+
};
430+
},
393431
shouldShowRecommendations() {
394432
if (!this.isAIFeatureEnabled) {
395433
return false;
@@ -586,6 +624,18 @@
586624
return this.isAnyFeedbackReasonSelected && this.isOtherFeedbackValid;
587625
},
588626
},
627+
watch: {
628+
isBrowsing(before, after) {
629+
if (before !== after) {
630+
this.$nextTick(() => this.handleJumpToSearch());
631+
}
632+
},
633+
browseChannelId(before, after) {
634+
if (before !== after) {
635+
this.$nextTick(() => this.handleJumpToSearch());
636+
}
637+
},
638+
},
589639
beforeRouteEnter(to, from, next) {
590640
next(vm => {
591641
vm.searchTerm = to.params.searchTerm || '';
@@ -625,6 +675,27 @@
625675
handleBackToBrowse() {
626676
this.$router.push(this.backToBrowseRoute);
627677
},
678+
handleJumpToRecommendations() {
679+
if (this.firstRecommendationRef) {
680+
this.firstRecommendationRef.focus();
681+
}
682+
},
683+
handleJumpToSearch() {
684+
if (this.isBrowsing) {
685+
if (this.browseChannelId) {
686+
this.$refs.contentTreeList.focus();
687+
} else {
688+
this.$refs.channelList.focus();
689+
}
690+
} else {
691+
this.$refs.searchResultList.focus();
692+
}
693+
},
694+
setFirstRecommendationRef(ref) {
695+
if (!this.firstRecommendationRef) {
696+
this.firstRecommendationRef = ref;
697+
}
698+
},
628699
updateLanguageQuery(language) {
629700
this.languageFromChannelList = language;
630701
},
@@ -724,6 +795,8 @@
724795
}
725796
},
726797
async loadRecommendations(belowThreshold) {
798+
this.firstRecommendationRef = null;
799+
727800
if (this.shouldShowRecommendations) {
728801
this.recommendationsLoading = true;
729802
this.recommendationsLoadingError = false;
@@ -930,6 +1003,10 @@
9301003
searchLabel: 'Search for resources…',
9311004
searchAction: 'Search',
9321005
savedSearchesLabel: 'View saved searches',
1006+
jumpToRecommendations: 'Jump to recommendations',
1007+
jumpToSearch: 'Jump to search',
1008+
jumpToSearchResults: 'Jump to search results',
1009+
jumpToTop: 'Jump to top',
9331010
9341011
// Copy strings
9351012
// undo: 'Undo',
@@ -978,4 +1055,12 @@
9781055
border-radius: 4px;
9791056
}
9801057
1058+
.keyboard-visibility {
1059+
cursor: default;
1060+
1061+
&:focus {
1062+
opacity: 1 !important;
1063+
}
1064+
}
1065+
9811066
</style>

0 commit comments

Comments
 (0)