Accessibility: Add Landmark navigation (#12190)
Co-authored-by: R Midhun Suresh <hi@midhun.dev>
This commit is contained in:
co-authored by
R Midhun Suresh
parent
4edf4e42cd
commit
3c9bd69d48
@@ -17,7 +17,7 @@ limitations under the License.
|
||||
*/
|
||||
|
||||
import { _td, TranslationKey } from "../languageHandler";
|
||||
import { IS_MAC, Key } from "../Keyboard";
|
||||
import { IS_MAC, IS_ELECTRON, Key } from "../Keyboard";
|
||||
import { IBaseSetting } from "../settings/Settings";
|
||||
import { KeyCombo } from "../KeyBindingsManager";
|
||||
|
||||
@@ -129,6 +129,10 @@ export enum KeyBindingAction {
|
||||
PreviousVisitedRoomOrSpace = "KeyBinding.PreviousVisitedRoomOrSpace",
|
||||
/** Navigates forward */
|
||||
NextVisitedRoomOrSpace = "KeyBinding.NextVisitedRoomOrSpace",
|
||||
/** Navigates to the next Landmark */
|
||||
NextLandmark = "KeyBinding.nextLandmark",
|
||||
/** Navigates to the next Landmark */
|
||||
PreviousLandmark = "KeyBinding.previousLandmark",
|
||||
|
||||
/** Toggles microphone while on a call */
|
||||
ToggleMicInCall = "KeyBinding.toggleMicInCall",
|
||||
@@ -291,6 +295,8 @@ export const CATEGORIES: Record<CategoryName, ICategory> = {
|
||||
KeyBindingAction.SwitchToSpaceByNumber,
|
||||
KeyBindingAction.PreviousVisitedRoomOrSpace,
|
||||
KeyBindingAction.NextVisitedRoomOrSpace,
|
||||
KeyBindingAction.NextLandmark,
|
||||
KeyBindingAction.PreviousLandmark,
|
||||
],
|
||||
},
|
||||
[CategoryName.AUTOCOMPLETE]: {
|
||||
@@ -714,4 +720,19 @@ export const KEYBOARD_SHORTCUTS: IKeyboardShortcuts = {
|
||||
key: Key.COMMA,
|
||||
},
|
||||
},
|
||||
[KeyBindingAction.NextLandmark]: {
|
||||
default: {
|
||||
ctrlOrCmdKey: !IS_ELECTRON,
|
||||
key: Key.F6,
|
||||
},
|
||||
displayName: _td("keyboard|next_landmark"),
|
||||
},
|
||||
[KeyBindingAction.PreviousLandmark]: {
|
||||
default: {
|
||||
ctrlOrCmdKey: !IS_ELECTRON,
|
||||
key: Key.F6,
|
||||
shiftKey: true,
|
||||
},
|
||||
displayName: _td("keyboard|prev_landmark"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2024 The Matrix.org Foundation C.I.C.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TimelineRenderingType } from "../contexts/RoomContext";
|
||||
import { Action } from "../dispatcher/actions";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
|
||||
export const enum Landmark {
|
||||
// This is the space/home button in the left panel.
|
||||
ACTIVE_SPACE_BUTTON,
|
||||
// This is the room filter in the left panel.
|
||||
ROOM_SEARCH,
|
||||
// This is the currently opened room/first room in the room list in the left panel.
|
||||
ROOM_LIST,
|
||||
// This is the message composer within the room if available or it is the welcome screen shown when no room is selected
|
||||
MESSAGE_COMPOSER_OR_HOME,
|
||||
}
|
||||
|
||||
const ORDERED_LANDMARKS = [
|
||||
Landmark.ACTIVE_SPACE_BUTTON,
|
||||
Landmark.ROOM_SEARCH,
|
||||
Landmark.ROOM_LIST,
|
||||
Landmark.MESSAGE_COMPOSER_OR_HOME,
|
||||
];
|
||||
|
||||
/**
|
||||
* The landmarks are cycled through in the following order:
|
||||
* ACTIVE_SPACE_BUTTON <-> ROOM_SEARCH <-> ROOM_LIST <-> MESSAGE_COMPOSER/HOME <-> ACTIVE_SPACE_BUTTON
|
||||
*/
|
||||
export class LandmarkNavigation {
|
||||
/**
|
||||
* Get the next/previous landmark that must be focused from a given landmark
|
||||
* @param currentLandmark The current landmark
|
||||
* @param backwards If true, the landmark before currentLandmark in ORDERED_LANDMARKS is returned
|
||||
* @returns The next landmark to focus
|
||||
*/
|
||||
private static getLandmark(currentLandmark: Landmark, backwards = false): Landmark {
|
||||
const currentIndex = ORDERED_LANDMARKS.findIndex((l) => l === currentLandmark);
|
||||
const offset = backwards ? -1 : 1;
|
||||
const newLandmark = ORDERED_LANDMARKS.at((currentIndex + offset) % ORDERED_LANDMARKS.length)!;
|
||||
return newLandmark;
|
||||
}
|
||||
|
||||
/**
|
||||
* Focus the next landmark from a given landmark.
|
||||
* This method will skip over any missing landmarks.
|
||||
* @param currentLandmark The current landmark
|
||||
* @param backwards If true, search the next landmark to the left in ORDERED_LANDMARKS
|
||||
*/
|
||||
public static findAndFocusNextLandmark(currentLandmark: Landmark, backwards = false): void {
|
||||
let landmark = currentLandmark;
|
||||
let element: HTMLElement | null | undefined = null;
|
||||
while (element === null) {
|
||||
landmark = LandmarkNavigation.getLandmark(landmark, backwards);
|
||||
element = landmarkToDomElementMap[landmark]();
|
||||
}
|
||||
element?.focus({ focusVisible: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The functions return:
|
||||
* - The DOM element of the landmark if it exists
|
||||
* - undefined if the DOM element exists but focus is given through an action
|
||||
* - null if the landmark does not exist
|
||||
*/
|
||||
const landmarkToDomElementMap: Record<Landmark, () => HTMLElement | null | undefined> = {
|
||||
[Landmark.ACTIVE_SPACE_BUTTON]: () => document.querySelector<HTMLElement>(".mx_SpaceButton_active"),
|
||||
|
||||
[Landmark.ROOM_SEARCH]: () => document.querySelector<HTMLElement>(".mx_RoomSearch"),
|
||||
[Landmark.ROOM_LIST]: () =>
|
||||
document.querySelector<HTMLElement>(".mx_RoomTile_selected") ||
|
||||
document.querySelector<HTMLElement>(".mx_RoomTile"),
|
||||
|
||||
[Landmark.MESSAGE_COMPOSER_OR_HOME]: () => {
|
||||
const isComposerOpen = !!document.querySelector(".mx_MessageComposer");
|
||||
if (isComposerOpen) {
|
||||
const inThread = !!document.activeElement?.closest(".mx_ThreadView");
|
||||
defaultDispatcher.dispatch(
|
||||
{
|
||||
action: Action.FocusSendMessageComposer,
|
||||
context: inThread ? TimelineRenderingType.Thread : TimelineRenderingType.Room,
|
||||
},
|
||||
true,
|
||||
);
|
||||
// Special case where the element does exist but we focus it through an action.
|
||||
return undefined;
|
||||
} else {
|
||||
return document.querySelector<HTMLElement>(".mx_HomePage");
|
||||
}
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user