Skip to content

Commit 7c4d758

Browse files
committed
Introduce a new setting to disable the keyboard shortcuts
1 parent c52571c commit 7c4d758

File tree

24 files changed

+148
-24
lines changed

24 files changed

+148
-24
lines changed

app/components/projects/settings/general/show_component.html.erb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
3333
settings_primer_form_with(model: project, url: project_settings_general_path(project)) do |f|
3434
concat(
3535
render(Primer::Beta::Subhead.new) do |component|
36-
component.with_heading(tag: :h2, size: :medium) { I18n.t("projects.settings.header_details") }
36+
component.with_heading(tag: :h3, size: :medium) { I18n.t("projects.settings.header_details") }
3737
end
3838
)
3939
concat(
@@ -52,7 +52,7 @@ See COPYRIGHT and LICENSE files for more details.
5252
settings_primer_form_with(model: project, url: project_settings_general_path(project)) do |f|
5353
concat(
5454
render(Primer::Beta::Subhead.new) do |component|
55-
component.with_heading(tag: :h2, size: :medium) { I18n.t("projects.settings.header_status") }
55+
component.with_heading(tag: :h3, size: :medium) { I18n.t("projects.settings.header_status") }
5656
end
5757
)
5858
concat(
@@ -71,7 +71,7 @@ See COPYRIGHT and LICENSE files for more details.
7171
settings_primer_form_with(model: project, url: project_settings_general_path(project)) do |f|
7272
concat(
7373
render(Primer::Beta::Subhead.new) do |component|
74-
component.with_heading(tag: :h2, size: :medium) { I18n.t("projects.settings.header_relations") }
74+
component.with_heading(tag: :h3, size: :medium) { I18n.t("projects.settings.header_relations") }
7575
end
7676
)
7777
concat(

app/forms/my/look_and_feel_form.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ class My::LookAndFeelForm < ApplicationForm
6262
end
6363
end
6464

65+
f.check_box name: :disable_keyboard_shortcuts,
66+
label: I18n.t("activerecord.attributes.user_preference.disable_keyboard_shortcuts"),
67+
caption: I18n.t("activerecord.attributes.user_preference.disable_keyboard_shortcuts_caption_html",
68+
href: OpenProject::Static::Links.links[:shortcuts][:href]).html_safe
69+
6570
f.submit(name: :submit,
6671
label: I18n.t("activerecord.attributes.user_preference.button_update_look_and_feel"),
6772
scheme: :default)

app/models/permitted_params.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,11 @@ def wiki_page
264264
end
265265

266266
def pref
267-
params.fetch(:pref, {}).permit(:time_zone, :theme,
268-
:comments_sorting, :warn_on_leaving_unsaved,
267+
params.fetch(:pref, {}).permit(:time_zone,
268+
:theme,
269+
:comments_sorting,
270+
:disable_keyboard_shortcuts,
271+
:warn_on_leaving_unsaved,
269272
:auto_hide_popups)
270273
end
271274

app/models/user_preference.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,14 @@ def comments_in_reverse_order?
8686
comments_sorting == "desc"
8787
end
8888

89+
def disable_keyboard_shortcuts?
90+
settings.fetch(:disable_keyboard_shortcuts) { Setting.disable_keyboard_shortcuts? }
91+
end
92+
93+
def disable_keyboard_shortcuts=(value)
94+
settings[:disable_keyboard_shortcuts] = to_boolean(value)
95+
end
96+
8997
def diff_type
9098
settings.fetch(:diff_type, "inline")
9199
end
@@ -110,6 +118,7 @@ def warn_on_leaving_unsaved=(value)
110118
alias :comments_in_reverse_order :comments_in_reverse_order?
111119
alias :warn_on_leaving_unsaved :warn_on_leaving_unsaved?
112120
alias :auto_hide_popups :auto_hide_popups?
121+
alias :disable_keyboard_shortcuts :disable_keyboard_shortcuts?
113122

114123
def comments_in_reverse_order=(value)
115124
settings[:comments_sorting] = to_boolean(value) ? "desc" : "asc"

app/views/my/interface.html.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ See COPYRIGHT and LICENSE files for more details.
4747
end
4848
%>
4949
<%=
50-
settings_primer_form_with(model: @user.pref, scope: :pref, url: { action: "update_settings" }) do |form|
50+
settings_primer_form_with(model: @user.pref, scope: :pref, url: { action: "update_settings" }, data: { turbo: false }) do |form|
5151
render(My::LookAndFeelForm.new(form))
5252
end
5353
%>
@@ -58,7 +58,7 @@ See COPYRIGHT and LICENSE files for more details.
5858
end
5959
%>
6060
<%=
61-
settings_primer_form_with(model: @user.pref, scope: :pref, url: { action: "update_settings" }) do |form|
61+
settings_primer_form_with(model: @user.pref, scope: :pref, url: { action: "update_settings" }, data: { turbo: false }) do |form|
6262
render(My::AlertsForm.new(form))
6363
end
6464
%>

app/views/users/_general.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ See COPYRIGHT and LICENSE files for more details.
4848
},
4949
data: {
5050
controller: "admin--users",
51+
turbo: false,
5152
"admin--users-password-auth-selected-value": @user.ldap_auth_source_id.blank?
5253
},
5354
as: :user do |f| %>

app/views/users/_preferences.html.erb

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,12 +39,15 @@ See COPYRIGHT and LICENSE files for more details.
3939
<%= I18n.t("activerecord.attributes.user_preference.mode_guideline") %>
4040
</div>
4141
</div>
42+
4243
<div class="form--field">
43-
<%= pref_fields.select :comments_sorting,
44-
[[t("activities.work_packages.activity_tab.label_sort_asc"), "asc"],
45-
[t("activities.work_packages.activity_tab.label_sort_desc"), "desc"]],
46-
container_class: defined?(input_size) ? "-#{input_size}" : "" %>
44+
<%= pref_fields.check_box :disable_keyboard_shortcuts,
45+
label: I18n.t("activerecord.attributes.user_preference.disable_keyboard_shortcuts") %>
46+
<span class="form--field-instructions">
47+
<%= I18n.t(
48+
"activerecord.attributes.user_preference.disable_keyboard_shortcuts_caption_html",
49+
href: OpenProject::Static::Links.links[:shortcuts][:href]
50+
).html_safe %>
51+
</span>
4752
</div>
48-
<div class="form--field"><%= pref_fields.check_box :warn_on_leaving_unsaved %></div>
49-
<div class="form--field"><%= pref_fields.check_box :auto_hide_popups %></div>
5053
<% end %>

app/views/users/new.html.erb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ See COPYRIGHT and LICENSE files for more details.
4848
html: { class: nil, autocomplete: "off" },
4949
data: {
5050
controller: "admin--users",
51+
turbo: false,
5152
"admin--users-password-auth-selected-value": @user.ldap_auth_source_id.blank?
5253
},
5354
as: :user do |f| %>

config/constants/settings/definition.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,10 @@ class Definition
333333
description: "Default sort order for activities",
334334
default: "asc"
335335
},
336+
disable_keyboard_shortcuts: {
337+
description: "Whether keyboard short cuts should be disabled (e.g. for better screen reader support)",
338+
default: false
339+
},
336340
default_language: {
337341
default: "en",
338342
allowed: -> { Redmine::I18n.all_languages }

config/locales/en.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1175,6 +1175,9 @@ en:
11751175
button_update_look_and_feel: "Update look and feel"
11761176
button_update_alerts: "Update alerts"
11771177
comments_sorting: "Display work package activity sorted by"
1178+
disable_keyboard_shortcuts: "Disable keyboard shortcuts"
1179+
disable_keyboard_shortcuts_caption_html: |-
1180+
You can choose to disable default <a href="%{href}">keyboard shortcuts</a> if you use a screen reader or want to avoid accidentally triggering an action with a shortcut.
11781181
dismissed_enterprise_banners: "Hidden enterprise banners"
11791182
impaired: "Accessibility mode"
11801183
auto_hide_popups: "Auto-hide success notifications"

config/schemas/user_preferences.schema.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@
1313
"type": "string",
1414
"enum": ["light", "light_high_contrast", "dark"]
1515
},
16+
"disable_keyboard_shortcuts": {
17+
"type": "boolean"
18+
},
1619
"warn_on_leaving_unsaved": {
1720
"type": "boolean"
1821
},

docs/api/apiv3/components/schemas/user_preferences_model.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ example:
1313
method: "patch"
1414
_type: UserPreferences
1515
commentSortDescending: true
16+
disableKeyboardShortcuts: false
1617
timeZone: "Europe/Berlin"
1718
warnOnLeavingUnsaved: true
1819
notifications:

docs/api/apiv3/paths/my_preferences.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ get:
1919
method: "patch"
2020
_type: "UserPreferences"
2121
commentSortDescending: true
22+
disableKeyboardShortcuts: false
2223
timeZone: "Europe/Berlin"
2324
warnOnLeavingUnsaved: true
2425
notifications:
@@ -82,6 +83,7 @@ patch:
8283
method: "patch"
8384
_type: UserPreferences
8485
commentSortDescending: true
86+
disableKeyboardShortcuts: false
8587
timeZone: Europe/Berlin
8688
warnOnLeavingUnsaved: true
8789
notifications:

frontend/src/app/core/config/configuration.service.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ export class ConfigurationService {
5151
return this.userPreference('commentSortDescending');
5252
}
5353

54+
public disableKeyboardShortcuts():boolean {
55+
return this.userPreference('disableKeyboardShortcuts');
56+
}
57+
5458
public warnOnLeavingUnsaved():boolean {
5559
return this.userPreference('warnOnLeavingUnsaved');
5660
}

frontend/src/app/features/user-preferences/state/user-preferences.model.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface ImmediateRemindersSettings {
1717
export interface IUserPreference {
1818
autoHidePopups:boolean;
1919
commentSortDescending:boolean;
20+
disableKeyboardShortcuts:boolean;
2021
timeZone:string|null;
2122
warnOnLeavingUnsaved:boolean;
2223
workdays:number[];

frontend/src/app/features/user-preferences/state/user-preferences.store.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ function createInitialState():IUserPreference {
3636
return {
3737
autoHidePopups: true,
3838
commentSortDescending: false,
39+
disableKeyboardShortcuts: false,
3940
timeZone: null,
4041
warnOnLeavingUnsaved: true,
4142
notifications: [],

frontend/src/app/shared/directives/a11y/keyboard-shortcut.service.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { FocusHelperService } from 'core-app/shared/directives/focus/focus-helpe
3131
import { PathHelperService } from 'core-app/core/path-helper/path-helper.service';
3232
import { CurrentProjectService } from 'core-app/core/current-project/current-project.service';
3333
import * as Mousetrap from 'mousetrap';
34+
import { ConfigurationService } from 'core-app/core/config/configuration.service';
3435

3536
const accessKeys = {
3637
preview: 1,
@@ -45,7 +46,6 @@ const accessKeys = {
4546

4647
// this could be extracted into a separate component if it grows
4748
const accessibleListSelector = 'table.keyboard-accessible-list';
48-
const accessibleRowSelector = 'table.keyboard-accessible-list tbody tr';
4949

5050
@Injectable({
5151
providedIn: 'root',
@@ -75,17 +75,22 @@ export class KeyboardShortcutService {
7575
/* eslint-enable quote-props */
7676
};
7777

78-
constructor(private readonly PathHelper:PathHelperService,
78+
constructor(
79+
private readonly PathHelper:PathHelperService,
7980
private readonly FocusHelper:FocusHelperService,
80-
private readonly currentProject:CurrentProjectService) {
81-
this.register();
82-
}
81+
private readonly currentProject:CurrentProjectService,
82+
private readonly configurationService:ConfigurationService,
83+
) {}
8384

8485
/**
8586
* Register the keyboard shortcuts.
8687
*/
8788
public register():void {
88-
_.each(this.shortcuts, (action:() => void, key:string) => Mousetrap.bind(key, action));
89+
void this.configurationService.initialize().then(() => {
90+
if (!this.configurationService.disableKeyboardShortcuts()) {
91+
_.each(this.shortcuts, (action:() => void, key:string) => Mousetrap.bind(key, action));
92+
}
93+
});
8994
}
9095

9196
public accessKey(keyName:'preview'|'newWorkPackage'|'edit'|'quickSearch'|'projectSearch'|'help'|'moreMenu'|'details'):() => void {

lib/api/v3/user_preferences/user_preference_representer.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ class UserPreferenceRepresenter < ::API::Decorators::Single
5656
property :time_zone,
5757
render_nil: true
5858

59+
property :disable_keyboard_shortcuts
60+
5961
property :warn_on_leaving_unsaved
6062
property :comments_in_reverse_order,
6163
as: :commentSortDescending

modules/backlogs/app/views/shared/_view_my_settings.html.erb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ See COPYRIGHT and LICENSE files for more details.
3333
end
3434
%>
3535
<%=
36-
settings_primer_form_with(model: user.pref, scope: :pref, url: { action: "update_settings" }) do |form|
36+
settings_primer_form_with(model: user.pref, scope: :pref, url: { action: "update_settings" }, data: { turbo: false }) do |form|
3737
render(My::BacklogsForm.new(form, color:, versions_default_fold_state:))
3838
end
3939
%>

spec/contracts/user_preferences/update_contract_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
},
5252
time_zone: "America/Sao_Paulo",
5353
warn_on_leaving_unsaved: true,
54+
disable_keyboard_shortcuts: true,
5455
workdays: [1, 2, 4, 6]
5556
}
5657
end

spec/controllers/my_controller_spec.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -230,17 +230,17 @@
230230
end
231231
end
232232

233-
describe "settings:auto_hide_popups" do
233+
describe "interface:auto_hide_popups" do
234234
context "with render_views" do
235235
before do
236236
as_logged_in_user user do
237-
get :settings
237+
get :interface
238238
end
239239
end
240240

241241
render_views
242242
it "renders auto hide popups checkbox" do
243-
expect(response.body).to have_css("#my_account_form #pref_auto_hide_popups")
243+
expect(response.body).to have_css("form #pref_auto_hide_popups")
244244
end
245245
end
246246

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# frozen_string_literal: true
2+
3+
#-- copyright
4+
# OpenProject is an open source project management software.
5+
# Copyright (C) the OpenProject GmbH
6+
#
7+
# This program is free software; you can redistribute it and/or
8+
# modify it under the terms of the GNU General Public License version 3.
9+
#
10+
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
11+
# Copyright (C) 2006-2013 Jean-Philippe Lang
12+
# Copyright (C) 2010-2013 the ChiliProject Team
13+
#
14+
# This program is free software; you can redistribute it and/or
15+
# modify it under the terms of the GNU General Public License
16+
# as published by the Free Software Foundation; either version 2
17+
# of the License, or (at your option) any later version.
18+
#
19+
# This program is distributed in the hope that it will be useful,
20+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
21+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22+
# GNU General Public License for more details.
23+
#
24+
# You should have received a copy of the GNU General Public License
25+
# along with this program; if not, write to the Free Software
26+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
27+
#
28+
# See COPYRIGHT and LICENSE files for more details.
29+
#++
30+
31+
require "spec_helper"
32+
33+
RSpec.describe "My account disable keyboard shortcuts setting", :js, :selenium do
34+
let(:user) { create(:user) }
35+
let(:global_search) { Components::GlobalSearch.new }
36+
37+
before do
38+
login_as(user)
39+
visit my_account_path
40+
end
41+
42+
it "can disable the keyboard short cuts" do
43+
click_on "Interface"
44+
45+
# Per default, the keyboard short cuts are enabled
46+
expect(page).to have_unchecked_field("Disable keyboard shortcuts", visible: :visible)
47+
page.driver.browser.switch_to.active_element.send_keys("s")
48+
global_search.expect_open
49+
50+
# Check the checkbox
51+
page.check("Disable keyboard shortcuts")
52+
expect(page).to have_checked_field("Disable keyboard shortcuts", visible: :visible)
53+
54+
# Save
55+
click_button accessible_name: "Update look and feel"
56+
wait_for_network_idle
57+
expect(page).to have_checked_field("Disable keyboard shortcuts", visible: :visible)
58+
59+
# Directly try to trigger the keyboard short cut again (which should not work any more)
60+
page.driver.browser.switch_to.active_element.send_keys("s")
61+
global_search.expect_closed
62+
63+
# After a hard reload, the setting is still remembered and turned off
64+
page.reload!
65+
wait_for_network_idle
66+
expect(page).to have_checked_field("Disable keyboard shortcuts", visible: :visible)
67+
page.driver.browser.switch_to.active_element.send_keys("s")
68+
global_search.expect_closed
69+
end
70+
end

spec/lib/api/v3/user_preferences/user_preference_representer_rendering_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656

5757
it { expect(subject).to have_json_path("timeZone") }
5858
it { expect(subject).to have_json_path("commentSortDescending") }
59+
it { expect(subject).to have_json_path("disableKeyboardShortcuts") }
5960
it { expect(subject).to have_json_path("warnOnLeavingUnsaved") }
6061
it { expect(subject).to have_json_path("autoHidePopups") }
6162

spec/support/components/global_search.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,11 @@ def submit_with_enter
4444
end
4545

4646
def expect_open
47-
expect(page).to have_selector(container)
47+
expect(page).to have_selector(selector)
48+
end
49+
50+
def expect_closed
51+
expect(page).to have_no_selector(selector)
4852
end
4953

5054
def submit_in_project_and_subproject_scope

0 commit comments

Comments
 (0)