This repository has been archived by the owner on Sep 11, 2024. It is now read-only.
-
-
Notifications
You must be signed in to change notification settings - Fork 834
Accessibility: Add Landmark navigation #12190
Merged
Merged
Changes from 34 commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
98dd07c
Override aria-label for whole tile
akirk 6e6696e
Message navigation
akirk b777dbd
Change hotkeys to use cmd/ctrl
akirk 3bb1bb4
Add landmark navigation
akirk 45cd0da
No cmd in Electron
akirk fd93d03
Fallback when no room is selected
akirk a954941
Review fixes
akirk 1eac6ff
Handle invisible events better
akirk 36048c1
Fix lint errors
akirk 4c2c31f
Lint fixes, remove stray code from other PR
akirk 645dc4c
Remove message navigation code
akirk f55ba53
Remove more stray code
akirk 9bdc225
lint fixes
akirk e34baa5
fix room list landmark
akirk 5ebd292
lint fixes
akirk d9d10d2
Remove copied function
akirk af40c89
update comment
akirk aee0c5f
lint
akirk 87ce6d2
Update keyboard user settings snapshot
akirk 87492e3
Merge branch 'develop' into add-landmark-navigation
MidhunSureshR e3ebda6
Fix lint
MidhunSureshR 7fba563
Move code to a single file
MidhunSureshR f3b7f12
Merge branch 'develop' into add-landmark-navigation
MidhunSureshR 7eb4dc6
Add jest test
MidhunSureshR 9608b75
Merge branch 'develop' into add-landmark-navigation
MidhunSureshR d043ae9
Use a circular array for storing the order of landmarks
MidhunSureshR e38849a
Fix test
MidhunSureshR 7edb9d4
Rename test
MidhunSureshR b493ac2
Change implementation
MidhunSureshR 1396862
Add playwright test
MidhunSureshR 477f58e
Add more tests
MidhunSureshR 6c8e191
Fix comments and name
MidhunSureshR b2ce73b
Replacee method with Array.at
MidhunSureshR ea4fea8
Make changes from review
MidhunSureshR 0961cae
Fix case; landmarkToDOMElementMap -> landmarkToDomElementMap
MidhunSureshR 114cc77
Add stricter check
MidhunSureshR 1d8ab2a
Add type to map
MidhunSureshR 9f29735
Pass focusVisible option to focus call
MidhunSureshR d1085b7
Move type to global.d.ts
MidhunSureshR File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
166 changes: 166 additions & 0 deletions
166
playwright/e2e/accessibility/keyboard-navigation.spec.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,166 @@ | ||
/* | ||
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 { test, expect } from "../../element-web-test"; | ||
import { Bot } from "../../pages/bot"; | ||
|
||
test.describe("Landmark navigation tests", () => { | ||
test.use({ | ||
displayName: "Alice", | ||
}); | ||
|
||
test("without any rooms", async ({ page, homeserver, app, user }) => { | ||
/** | ||
* Without any rooms, there is no tile in the roomlist to be focused. | ||
* So the next landmark in the list should be focused instead. | ||
*/ | ||
|
||
// Pressing Control+F6 will first focus the space button | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus room search | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_RoomSearch")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus the message composer | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_HomePage")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will bring focus back to the space button | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
|
||
// Now go back in the same order | ||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_HomePage")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_RoomSearch")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
}); | ||
|
||
test("with an open room", async ({ page, homeserver, app, user }) => { | ||
const bob = new Bot(page, homeserver, { displayName: "Bob" }); | ||
await bob.prepareClient(); | ||
|
||
// create dm with bob | ||
await app.client.evaluate( | ||
async (cli, { bob }) => { | ||
const bobRoom = await cli.createRoom({ is_direct: true }); | ||
await cli.invite(bobRoom.room_id, bob); | ||
}, | ||
{ | ||
bob: bob.credentials.userId, | ||
}, | ||
); | ||
|
||
await app.viewRoomByName("Bob"); | ||
// confirm the room was loaded | ||
await expect(page.getByText("Bob joined the room")).toBeVisible(); | ||
|
||
// Pressing Control+F6 will first focus the space button | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus room search | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_RoomSearch")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus the room tile in the room list | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_RoomTile_selected")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus the message composer | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_BasicMessageComposer_input")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will bring focus back to the space button | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
|
||
// Now go back in the same order | ||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_BasicMessageComposer_input")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_RoomTile_selected")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_RoomSearch")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
}); | ||
|
||
test("without an open room", async ({ page, homeserver, app, user }) => { | ||
const bob = new Bot(page, homeserver, { displayName: "Bob" }); | ||
await bob.prepareClient(); | ||
|
||
// create a dm with bob | ||
await app.client.evaluate( | ||
async (cli, { bob }) => { | ||
const bobRoom = await cli.createRoom({ is_direct: true }); | ||
await cli.invite(bobRoom.room_id, bob); | ||
}, | ||
{ | ||
bob: bob.credentials.userId, | ||
}, | ||
); | ||
|
||
await app.viewRoomByName("Bob"); | ||
// confirm the room was loaded | ||
await expect(page.getByText("Bob joined the room")).toBeVisible(); | ||
|
||
// Close the room | ||
page.goto("/#/home"); | ||
|
||
// Pressing Control+F6 will first focus the space button | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus room search | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_RoomSearch")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus the room tile in the room list | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_RoomTile")).toBeFocused(); | ||
|
||
// Pressing Control+F6 again will focus the home section | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_HomePage")).toBeFocused(); | ||
|
||
// Pressing Control+F6 will bring focus back to the space button | ||
await page.keyboard.press("ControlOrMeta+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
|
||
// Now go back in same order | ||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_HomePage")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_RoomTile")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_RoomSearch")).toBeFocused(); | ||
|
||
await page.keyboard.press("ControlOrMeta+Shift+F6"); | ||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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(); | ||
} | ||
} | ||
|
||
/** | ||
* 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 = { | ||
[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"); | ||
} | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry being pedantic: https://www.approxion.com/capital-offenses-how-to-handle-abbreviations-in-camelcase/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this type will prevent us forgetting to add an entry if we add to the enum
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0961cae and 1d8ab2a