element-web/src/accessibility/LandmarkNavigation.ts
David Langley 69ee8fd96a
Change License: AGPL + Element Commercial (#28856)
* Add commercial licence and update config files

* Update license in headers

* Revert "Update license in headers"

This reverts commit 7ed7949485e88889a9ffc8075a9df1f8e936777e.

* Update only spdx id

* Remove LicenseRef- from package.json

LicenseRef- no longer allowed in npm v3 package.json
This fixes the warning in the logs and failing build check.
2025-01-06 11:18:54 +00:00

98 lines
3.9 KiB
TypeScript

/*
* Copyright 2024 New Vector Ltd.
* Copyright 2024 The Matrix.org Foundation C.I.C.
*
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
* Please see LICENSE files in the repository root for full details.
*/
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");
}
},
};