Skip to content

Commit 447eddc

Browse files
fix(ui): make keyboard shortcut framework generic
Signed-off-by: Sudhanshu Singh <sudhanshuwriterblc@gmail.com>
1 parent b80742e commit 447eddc

4 files changed

Lines changed: 90 additions & 88 deletions

File tree

templates/repo/blame.tmpl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,15 @@
1919
<div class="ui buttons">
2020
<a class="ui tiny button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
2121
{{if or .RefFullName.IsBranch .RefFullName.IsTag}}
22-
<a id="file-permalink-button" class="ui tiny button" href="{{.RepoLink}}/src/commit/{{.CommitID | PathEscape}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
22+
<div class="global-shortcut-wrapper tw-inline-block">
23+
<a class="ui tiny button" href="{{.RepoLink}}/src/commit/{{.CommitID | PathEscape}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
24+
<kbd data-global-init="onGlobalShortcut" data-shortcut-keys="y" class="tw-hidden">Y</kbd>
25+
</div>
2326
{{end}}
24-
<a id="file-blame-button" class="ui tiny button" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.normal_view"}}</a>
27+
<div class="global-shortcut-wrapper tw-inline-block">
28+
<a class="ui tiny button" href="{{.RepoLink}}/src/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.normal_view"}}</a>
29+
<kbd data-global-init="onGlobalShortcut" data-shortcut-keys="b" class="tw-hidden">B</kbd>
30+
</div>
2531
<a class="ui tiny button" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}/{{.TreePath | PathEscapeSegments}}">{{ctx.Locale.Tr "repo.file_history"}}</a>
2632
<button class="ui tiny button unescape-button">{{ctx.Locale.Tr "repo.unescape_control_characters"}}</button>
2733
<button class="ui tiny button escape-button tw-hidden">{{ctx.Locale.Tr "repo.escape_control_characters"}}</button>

templates/repo/branch_dropdown.tmpl

Lines changed: 47 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,58 @@
1414
1515
Search "repo/branch_dropdown" in the template directory to find all occurrences.
1616
*/}}
17-
<div class="{{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"
18-
data-global-init="initRepoBranchTagSelector"
19-
data-text-release-compare="{{ctx.Locale.Tr "repo.release.compare"}}"
20-
data-text-branches="{{ctx.Locale.Tr "repo.branches"}}"
21-
data-text-tags="{{ctx.Locale.Tr "repo.tags"}}"
22-
data-text-filter-branch="{{ctx.Locale.Tr "repo.pulls.filter_branch"}}"
23-
data-text-filter-tag="{{ctx.Locale.Tr "repo.find_tag"}}"
24-
data-text-default-branch-label="{{ctx.Locale.Tr "repo.default_branch_label"}}"
25-
data-text-create-tag="{{ctx.Locale.Tr "repo.tag.create_tag"}}"
26-
data-text-create-branch="{{ctx.Locale.Tr "repo.branch.create_branch"}}"
27-
data-text-create-ref-from="{{ctx.Locale.Tr "repo.branch.create_from"}}"
28-
data-text-no-results="{{ctx.Locale.Tr "no_results_found"}}"
29-
data-text-view-all-branches="{{ctx.Locale.Tr "repo.view_all_branches"}}"
30-
data-text-view-all-tags="{{ctx.Locale.Tr "repo.view_all_tags"}}"
17+
<div class="global-shortcut-wrapper tw-inline-block">
18+
<div class="{{if .ContainerClasses}}{{.ContainerClasses}}{{end}}"
19+
data-global-init="initRepoBranchTagSelector"
20+
data-text-release-compare="{{ctx.Locale.Tr "repo.release.compare"}}"
21+
data-text-branches="{{ctx.Locale.Tr "repo.branches"}}"
22+
data-text-tags="{{ctx.Locale.Tr "repo.tags"}}"
23+
data-text-filter-branch="{{ctx.Locale.Tr "repo.pulls.filter_branch"}}"
24+
data-text-filter-tag="{{ctx.Locale.Tr "repo.find_tag"}}"
25+
data-text-default-branch-label="{{ctx.Locale.Tr "repo.default_branch_label"}}"
26+
data-text-create-tag="{{ctx.Locale.Tr "repo.tag.create_tag"}}"
27+
data-text-create-branch="{{ctx.Locale.Tr "repo.branch.create_branch"}}"
28+
data-text-create-ref-from="{{ctx.Locale.Tr "repo.branch.create_from"}}"
29+
data-text-no-results="{{ctx.Locale.Tr "no_results_found"}}"
30+
data-text-view-all-branches="{{ctx.Locale.Tr "repo.view_all_branches"}}"
31+
data-text-view-all-tags="{{ctx.Locale.Tr "repo.view_all_tags"}}"
3132

32-
data-current-repo-default-branch="{{.Repository.DefaultBranch}}"
33-
data-current-repo-link="{{.Repository.Link}}"
34-
data-current-tree-path="{{.CurrentTreePath}}"
35-
data-current-ref-type="{{.CurrentRefType}}"
36-
data-current-ref-short-name="{{.CurrentRefShortName}}"
33+
data-current-repo-default-branch="{{.Repository.DefaultBranch}}"
34+
data-current-repo-link="{{.Repository.Link}}"
35+
data-current-tree-path="{{.CurrentTreePath}}"
36+
data-current-ref-type="{{.CurrentRefType}}"
37+
data-current-ref-short-name="{{.CurrentRefShortName}}"
3738

38-
data-ref-link-template="{{.RefLinkTemplate}}"
39-
data-ref-form-action-template="{{.RefFormActionTemplate}}"
40-
data-dropdown-fixed-text="{{.DropdownFixedText}}"
41-
data-show-tab-branches="{{.ShowTabBranches}}"
42-
data-show-tab-tags="{{.ShowTabTags}}"
43-
data-allow-create-new-ref="{{.AllowCreateNewRef}}"
44-
data-show-view-all-refs-entry="{{.ShowViewAllRefsEntry}}"
39+
data-ref-link-template="{{.RefLinkTemplate}}"
40+
data-ref-form-action-template="{{.RefFormActionTemplate}}"
41+
data-dropdown-fixed-text="{{.DropdownFixedText}}"
42+
data-show-tab-branches="{{.ShowTabBranches}}"
43+
data-show-tab-tags="{{.ShowTabTags}}"
44+
data-allow-create-new-ref="{{.AllowCreateNewRef}}"
45+
data-show-view-all-refs-entry="{{.ShowViewAllRefsEntry}}"
4546

46-
data-enable-feed="{{ctx.RootData.EnableFeed}}"
47-
>
48-
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
49-
<div class="ui dropdown custom branch-selector-dropdown ellipsis-text-items">
50-
<div class="ui compact button branch-dropdown-button">
51-
<span class="flex-text-block gt-ellipsis">
52-
{{if .DropdownFixedText}}
53-
{{.DropdownFixedText}}
54-
{{else}}
55-
{{if eq .CurrentRefType "tag"}}
56-
{{svg "octicon-tag"}}
57-
{{else if eq .CurrentRefType "branch"}}
58-
{{svg "octicon-git-branch"}}
47+
data-enable-feed="{{ctx.RootData.EnableFeed}}"
48+
>
49+
{{/* show dummy elements before Vue componment is mounted, this code must match the code in BranchTagSelector.vue */}}
50+
<div class="ui dropdown custom branch-selector-dropdown ellipsis-text-items">
51+
<div class="ui compact button branch-dropdown-button">
52+
<span class="flex-text-block gt-ellipsis">
53+
{{if .DropdownFixedText}}
54+
{{.DropdownFixedText}}
5955
{{else}}
60-
{{svg "octicon-git-commit"}}
56+
{{if eq .CurrentRefType "tag"}}
57+
{{svg "octicon-tag"}}
58+
{{else if eq .CurrentRefType "branch"}}
59+
{{svg "octicon-git-branch"}}
60+
{{else}}
61+
{{svg "octicon-git-commit"}}
62+
{{end}}
63+
<strong class="tw-inline-block gt-ellipsis">{{.CurrentRefShortName}}</strong>
6164
{{end}}
62-
<strong class="tw-inline-block gt-ellipsis">{{.CurrentRefShortName}}</strong>
63-
{{end}}
64-
</span>
65-
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
65+
</span>
66+
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
67+
</div>
6668
</div>
6769
</div>
70+
<kbd data-global-init="onGlobalShortcut" data-shortcut-keys="w" class="tw-hidden">W</kbd>
6871
</div>

templates/repo/view_file.tmpl

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,16 @@
4848
<div class="ui buttons tw-mr-1">
4949
<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
5050
{{if or .RefFullName.IsBranch .RefFullName.IsTag}}
51-
<a id="file-permalink-button" class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
51+
<div class="global-shortcut-wrapper tw-inline-block">
52+
<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
53+
<kbd data-global-init="onGlobalShortcut" data-shortcut-keys="y" class="tw-hidden">Y</kbd>
54+
</div>
5255
{{end}}
5356
{{if .IsRepresentableAsText}}
54-
<a id="file-blame-button" class="ui mini basic button" href="{{.RepoLink}}/blame/{{.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.blame"}}</a>
57+
<div class="global-shortcut-wrapper tw-inline-block">
58+
<a class="ui mini basic button" href="{{.RepoLink}}/blame/{{.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.blame"}}</a>
59+
<kbd data-global-init="onGlobalShortcut" data-shortcut-keys="b" class="tw-hidden">B</kbd>
60+
</div>
5561
{{end}}
5662
<a class="ui mini basic button" href="{{.RepoLink}}/commits/{{.RefTypeNameSubURL}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_history"}}</a>
5763
{{if .EscapeStatus.Escaped}}

web_src/js/modules/shortcut.ts

Lines changed: 27 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ function shortcutWrapper(el: HTMLElement): HTMLElement | null {
1515
return parent?.matches('.global-shortcut-wrapper') ? parent : null;
1616
}
1717

18-
function elemFromKbd(kbd: HTMLElement): HTMLInputElement | HTMLTextAreaElement | null {
19-
return shortcutWrapper(kbd)?.querySelector<HTMLInputElement>('input, textarea') || null;
18+
function elemFromKbd(kbd: HTMLElement): HTMLElement | null {
19+
return shortcutWrapper(kbd)?.querySelector<HTMLElement>('input, textarea, select, a, button, .button') || null;
2020
}
2121

2222
function kbdFromElem(input: HTMLElement): HTMLElement | null {
@@ -108,10 +108,9 @@ export function initGlobalShortcut() {
108108
});
109109
}
110110

111-
// A <kbd> element next to an <input> declares a keyboard shortcut for that input.
112-
// When the matching key is pressed, the sibling input is focused.
113-
// When Escape is pressed inside such an input, the input is cleared and blurred.
114-
// The <kbd> element is shown/hidden automatically based on input focus and value.
111+
// A <kbd> element next to an element declares a keyboard shortcut for that element.
112+
// When the matching key is pressed, inputs are focused and buttons/links are clicked.
113+
// When Escape is pressed inside an input, the input is cleared and blurred.
115114
document.addEventListener('keydown', (e: KeyboardEvent) => {
116115
// Modifier keys are not supported yet
117116
if (e.ctrlKey || e.metaKey || e.altKey) return;
@@ -153,55 +152,43 @@ export function initGlobalShortcut() {
153152
return;
154153
}
155154

156-
// Handle specific page shortcuts
157-
if (key === 'w') {
158-
const branchDropdown = document.querySelector<HTMLElement>('.branch-dropdown-button');
159-
if (branchDropdown) {
160-
e.preventDefault();
161-
branchDropdown.click();
162-
return;
163-
}
164-
} else if (key === 'y') {
165-
const permalinkBtn = document.querySelector<HTMLAnchorElement>('#file-permalink-button');
166-
if (permalinkBtn) {
167-
e.preventDefault();
168-
window.location.assign(permalinkBtn.href);
169-
return;
170-
}
171-
} else if (key === 'b') {
172-
const blameBtn = document.querySelector<HTMLAnchorElement>('#file-blame-button');
173-
if (blameBtn) {
174-
e.preventDefault();
175-
window.location.assign(blameBtn.href);
176-
return;
177-
}
178-
}
179-
180155
// Handle sequence shortcuts (g + key)
181156
if (handleSequenceShortcut(key)) {
182157
e.preventDefault();
183158
return;
184159
}
185160

186-
// Find kbd element with matching shortcut (case-insensitive), then focus its sibling input
187-
// At the moment, only a simple match. In the future, it can be extended to support modifiers and key combinations
161+
// Find kbd element with matching shortcut (case-insensitive), then focus or click its target element
188162
const kbd = document.querySelector<HTMLElement>(`.global-shortcut-wrapper > kbd[data-shortcut-keys="${CSS.escape(key)}"]`);
189163
if (!kbd) return;
164+
165+
const elem = elemFromKbd(kbd);
166+
if (!elem) return;
167+
190168
e.preventDefault();
191-
elemFromKbd(kbd)!.focus();
169+
if (elem.matches('input, textarea, select')) {
170+
elem.focus();
171+
} else {
172+
elem.click();
173+
}
192174
});
193175

194176
// Toggle kbd shortcut hint visibility on input focus/blur
195177
document.addEventListener('focusin', (e) => {
196-
const kbd = kbdFromElem(e.target as HTMLElement);
197-
if (!kbd) return;
198-
hideElem(kbd);
178+
const target = e.target as HTMLElement;
179+
if (target.matches('input, textarea')) {
180+
const kbd = kbdFromElem(target);
181+
if (kbd) hideElem(kbd);
182+
}
199183
});
200184

201185
document.addEventListener('focusout', (e) => {
202-
const kbd = kbdFromElem(e.target as HTMLElement);
203-
if (!kbd) return;
204-
const hasContent = Boolean((e.target as HTMLInputElement).value);
205-
toggleElem(kbd, !hasContent);
186+
const target = e.target as HTMLElement;
187+
if (target.matches('input, textarea')) {
188+
const kbd = kbdFromElem(target);
189+
if (!kbd) return;
190+
const hasContent = Boolean((target as HTMLInputElement).value);
191+
toggleElem(kbd, !hasContent);
192+
}
206193
});
207194
}

0 commit comments

Comments
 (0)