gear_menu: Migrate to use tippy.

This commit is contained in:
Aman Agrawal 2023-10-13 10:51:35 +00:00 committed by Tim Abbott
parent 1e93540d3b
commit bc3d48616e
20 changed files with 295 additions and 291 deletions

View File

@ -548,7 +548,8 @@ export async function manage_organization(page: Page): Promise<void> {
await page.waitForSelector(menu_selector, {visible: true});
await page.click(menu_selector);
const organization_settings = '.dropdown-menu a[href="#organization"]';
const organization_settings = '.link-item a[href="#organization"]';
await page.waitForSelector(organization_settings, {visible: true});
await page.click(organization_settings);
await page.waitForSelector("#settings_overlay_container.show", {visible: true});

View File

@ -42,7 +42,7 @@ async function navigate_to_subscriptions(page: Page): Promise<void> {
await open_menu(page);
const manage_streams_selector = '.dropdown-menu a[href^="#streams"]';
const manage_streams_selector = '.link-item a[href^="#streams"]';
await page.waitForSelector(manage_streams_selector, {visible: true});
await page.click(manage_streams_selector);

View File

@ -8,7 +8,11 @@ async function navigate_to_user_list(page: Page): Promise<void> {
const menu_selector = "#settings-dropdown";
await page.waitForSelector(menu_selector, {visible: true});
await page.click(menu_selector);
await page.click('.dropdown-menu a[href="#organization"]');
const organization_settings = '.link-item a[href="#organization"]';
await page.waitForSelector(organization_settings, {visible: true});
await page.click(organization_settings);
await page.waitForSelector("#settings_overlay_container.show", {visible: true});
await page.click("li[data-section='user-list-admin']");
}

View File

@ -1,22 +1,18 @@
import $ from "jquery";
import tippy from "tippy.js";
import WinChan from "winchan";
// You won't find every click handler here, but it's a good place to start!
import render_buddy_list_tooltip_content from "../templates/buddy_list_tooltip_content.hbs";
import * as activity_ui from "./activity_ui";
import * as blueslip from "./blueslip";
import * as browser_history from "./browser_history";
import * as buddy_data from "./buddy_data";
import * as channel from "./channel";
import * as compose from "./compose";
import * as compose_actions from "./compose_actions";
import * as compose_reply from "./compose_reply";
import * as compose_state from "./compose_state";
import {media_breakpoints_num} from "./css_variables";
import * as dark_theme from "./dark_theme";
import * as emoji_picker from "./emoji_picker";
import * as hash_util from "./hash_util";
import * as hashchange from "./hashchange";
@ -760,48 +756,6 @@ export function initialize() {
},
);
// WEBATHENA
$("body").on("click", ".webathena_login", (e) => {
$("#zephyr-mirror-error").removeClass("show");
const principal = ["zephyr", "zephyr"];
WinChan.open(
{
url: "https://webathena.mit.edu/#!request_ticket_v1",
relay_url: "https://webathena.mit.edu/relay.html",
params: {
realm: "ATHENA.MIT.EDU",
principal,
},
},
(err, r) => {
if (err) {
blueslip.warn(err);
return;
}
if (r.status !== "OK") {
blueslip.warn(r);
return;
}
channel.post({
url: "/accounts/webathena_kerberos_login/",
data: {cred: JSON.stringify(r.session)},
success() {
$("#zephyr-mirror-error").removeClass("show");
},
error() {
$("#zephyr-mirror-error").addClass("show");
},
});
},
);
$("#settings-dropdown").dropdown("toggle");
e.preventDefault();
e.stopPropagation();
});
// End Webathena code
// disable the draggability for left-sidebar components
$("#stream_filters, #left-sidebar-navigation-list").on("dragstart", (e) => {
e.target.blur();
@ -823,36 +777,12 @@ export function initialize() {
// Don't focus links on context menu.
$("body").on("contextmenu", "a", (e) => e.target.blur());
// GEAR MENU
$("body").on("click", ".change-language-spectator, .language_selection_widget button", (e) => {
$("body").on("click", ".language_selection_widget button", (e) => {
e.preventDefault();
e.stopPropagation();
settings_display.launch_default_language_setting_modal();
});
// We cannot update recipient bar color using dark_theme.enable/disable due to
// it being called before message lists are initialized and the order cannot be changed.
// Also, since these buttons are only visible for spectators which doesn't have events,
// if theme is changed in a different tab, the theme of this tab remains the same.
$("body").on("click", "#gear-menu .dark-theme", (e) => {
// Allow propagation to close gear menu.
e.preventDefault();
requestAnimationFrame(() => {
dark_theme.enable();
message_lists.update_recipient_bar_background_color();
});
});
$("body").on("click", "#gear-menu .light-theme", (e) => {
// Allow propagation to close gear menu.
e.preventDefault();
requestAnimationFrame(() => {
dark_theme.disable();
message_lists.update_recipient_bar_background_color();
});
});
$("body").on("click", "#header-container .brand", (e) => {
e.preventDefault();
e.stopPropagation();

View File

@ -1,8 +1,17 @@
import $ from "jquery";
import WinChan from "winchan";
import render_gear_menu from "../templates/gear_menu.hbs";
import render_gear_menu_popover from "../templates/gear_menu_popover.hbs";
import * as blueslip from "./blueslip";
import * as channel from "./channel";
import * as dark_theme from "./dark_theme";
import * as message_lists from "./message_lists";
import * as popover_menus from "./popover_menus";
import * as popover_menus_data from "./popover_menus_data";
import * as popovers from "./popovers";
import * as settings_display from "./settings_display";
import {parse_html} from "./ui_util";
/*
For various historical reasons there isn't one
@ -77,23 +86,132 @@ The click handler uses "[data-overlay-trigger]" as
the selector and then calls browser_history.go_to_location.
*/
function render(instance) {
const rendered_gear_menu = render_gear_menu_popover(
popover_menus_data.get_gear_menu_content_context(),
);
instance.setContent(parse_html(rendered_gear_menu));
}
export function initialize() {
const rendered_gear_menu = render_gear_menu(popover_menus_data.get_gear_menu_content_context());
$("#navbar-buttons").html(rendered_gear_menu);
popover_menus.register_popover_menu("#gear-menu", {
placement: "bottom",
offset: [-50, 0],
popperOptions: {
strategy: "fixed",
modifiers: [
{
name: "eventListeners",
options: {
scroll: false,
},
},
],
},
onMount(instance) {
const $popper = $(instance.popper);
$popper.addClass("navbar-dropdown-tippy");
popover_menus.popover_instances.gear_menu = instance;
$(".focus-dropdown").on("focus", (e) => {
e.preventDefault();
$("#gear-menu-dropdown").find(".org-version a").trigger("focus");
});
$popper.on("click", ".webathena_login", (e) => {
$("#zephyr-mirror-error").removeClass("show");
const principal = ["zephyr", "zephyr"];
WinChan.open(
{
url: "https://webathena.mit.edu/#!request_ticket_v1",
relay_url: "https://webathena.mit.edu/relay.html",
params: {
realm: "ATHENA.MIT.EDU",
principal,
},
},
(err, r) => {
if (err) {
blueslip.warn(err);
return;
}
if (r.status !== "OK") {
blueslip.warn(r);
return;
}
channel.post({
url: "/accounts/webathena_kerberos_login/",
data: {cred: JSON.stringify(r.session)},
success() {
$("#zephyr-mirror-error").removeClass("show");
},
error() {
$("#zephyr-mirror-error").addClass("show");
},
});
},
);
instance.hide();
e.preventDefault();
e.stopPropagation();
});
$popper.on("click", ".change-language-spectator", (e) => {
instance.hide();
e.preventDefault();
e.stopPropagation();
settings_display.launch_default_language_setting_modal();
});
// We cannot update recipient bar color using dark_theme.enable/disable due to
// it being called before message lists are initialized and the order cannot be changed.
// Also, since these buttons are only visible for spectators which doesn't have events,
// if theme is changed in a different tab, the theme of this tab remains the same.
$popper.on("click", "#gear-menu-dropdown .dark-theme", (e) => {
instance.hide();
e.preventDefault();
e.stopPropagation();
requestAnimationFrame(() => {
dark_theme.enable();
message_lists.update_recipient_bar_background_color();
});
});
$popper.on("click", "#gear-menu-dropdown .light-theme", (e) => {
instance.hide();
e.preventDefault();
e.stopPropagation();
requestAnimationFrame(() => {
dark_theme.disable();
message_lists.update_recipient_bar_background_color();
});
});
},
onShow: render,
onHidden(instance) {
instance.destroy();
popover_menus.popover_instances.gear_menu = undefined;
},
});
}
export function open() {
$("#settings-dropdown").trigger("click");
// there are invisible li tabs, which should not be clicked.
$("#gear-menu").find("li:not(.invisible) a").eq(0).trigger("focus");
export function toggle() {
if (popover_menus.is_gear_menu_popover_displayed()) {
popovers.hide_all();
return;
}
// Since this can be called via hotkey, we need to
// hide any other popovers that may be open before.
if (popovers.any_active()) {
popovers.hide_all();
}
$("#gear-menu").trigger("click");
}
export function is_open() {
return $(".dropdown").hasClass("open");
}
export function close() {
if (is_open()) {
$(".dropdown").removeClass("open");
export function rerender() {
if (popover_menus.is_gear_menu_popover_displayed()) {
render(popover_menus.get_gear_menu_instance());
}
}

View File

@ -289,11 +289,6 @@ export function process_escape_key(e) {
return true;
}
if (gear_menu.is_open()) {
gear_menu.close();
return true;
}
if (processing_text()) {
if (activity_ui.searching()) {
activity_ui.escape_search();
@ -736,12 +731,12 @@ export function process_hotkey(e, hotkey) {
return false;
}
if (hotkey.message_view_only && gear_menu.is_open()) {
if (hotkey.message_view_only && popover_menus.is_gear_menu_popover_displayed()) {
// Inside the gear menu, we don't process most hotkeys; the
// exception is that the gear_menu hotkey should toggle the
// menu closed again.
if (event_name === "gear_menu") {
gear_menu.close();
gear_menu.toggle();
return true;
}
return false;
@ -897,7 +892,7 @@ export function process_hotkey(e, hotkey) {
search.initiate_search();
return true;
case "gear_menu":
gear_menu.open();
gear_menu.toggle();
return true;
case "show_shortcuts": // Show keyboard shortcuts page
browser_history.go_to_location("keyboard-shortcuts");

View File

@ -275,11 +275,6 @@ function open_invite_user_modal(e: JQuery.ClickEvent<Document, undefined>): void
e.stopPropagation();
e.preventDefault();
const $gear_menu = $("#gear-menu");
if ($gear_menu.hasClass("open")) {
$gear_menu.removeClass("open");
}
const time_unit_choices = ["minutes", "hours", "days", "weeks"];
const html_body = render_invite_user_modal({
is_admin: page_params.is_admin,

View File

@ -3,7 +3,6 @@ import tippy from "tippy.js";
import render_personal_menu from "../templates/personal_menu.hbs";
import * as gear_menu from "./gear_menu";
import * as narrow from "./narrow";
import {page_params} from "./page_params";
import * as people from "./people";
@ -38,13 +37,9 @@ export function initialize() {
},
onMount(instance) {
const $popper = $(instance.popper);
$popper.addClass("personal-menu-tippy");
$popper.addClass("navbar-dropdown-tippy");
popover_menus.popover_instances.personal_menu = instance;
// Workaround for the gear menu not being a tippy popover
// and thus not auto-closing.
gear_menu.close();
tippy(".clear_status", {
placement: "top",
appendTo: document.body,

View File

@ -36,6 +36,7 @@ export const popover_instances = {
send_later: null,
change_visibility_policy: null,
personal_menu: null,
gear_menu: null,
};
/* Keyboard UI functions */
@ -98,6 +99,14 @@ export function is_compose_enter_sends_popover_displayed() {
return popover_instances.compose_enter_sends?.state.isVisible;
}
export function is_gear_menu_popover_displayed() {
return popover_instances.gear_menu?.state.isVisible;
}
export function get_gear_menu_instance() {
return popover_instances.gear_menu;
}
function get_popover_items_for_instance(instance) {
const $current_elem = $(instance.popper);
const class_name = $current_elem.attr("class");

View File

@ -258,7 +258,7 @@ export function dispatch_normal_event(event) {
if (event.property === "invite_to_realm_policy") {
settings_invites.update_invite_user_panel();
sidebar_ui.update_invite_user_option();
gear_menu.initialize();
gear_menu.rerender();
}
const stream_creation_settings = [
@ -289,7 +289,7 @@ export function dispatch_normal_event(event) {
if (key === "create_multiuse_invite_group") {
settings_invites.update_invite_user_panel();
sidebar_ui.update_invite_user_option();
gear_menu.initialize();
gear_menu.rerender();
}
if (key === "edit_topic_policy") {

View File

@ -674,12 +674,6 @@ div.overlay {
}
}
.white_zulip_icon_without_text {
display: inline-block;
background-image: url("../images/logo/white-zulip-logo-without-text.svg");
background-size: cover;
}
.only-visible-for-spectators {
display: none;
}

View File

@ -691,13 +691,6 @@
opacity: 0.2;
}
#gear_menu_about_zulip {
.white_zulip_icon_without_text {
filter: invert(10%) sepia(16%) saturate(175%) hue-rotate(194deg)
brightness(99%) contrast(89%);
}
}
.spectator-view #gear-menu-dropdown {
.dark-theme {
display: none;
@ -706,11 +699,6 @@
.light-theme {
display: block;
}
#gear-menu {
.dropdown-menu a:hover {
color: hsl(0deg 0% 100%);
}
}
.nav .dropdown-menu::after {

View File

@ -1034,6 +1034,10 @@ ul {
}
}
#personal-menu-dropdown {
padding-bottom: 5px;
}
.personal-menu-header {
display: flex;
flex-flow: row nowrap;
@ -1192,6 +1196,92 @@ ul {
width: 16px;
height: 16px;
}
.zulip-icon {
position: relative;
top: -1px;
}
}
#gear-menu-dropdown {
padding: 5px 0;
box-shadow: var(--box-shadow-gear-menu);
.org-info {
padding: 4px 0 5px;
& li {
display: flex;
justify-content: center;
font-size: 15px;
font-style: normal;
font-weight: 400;
line-height: 16px; /* 114.286% */
letter-spacing: 0.28px;
&:focus-within {
background: var(--color-background-hover-dropdown-menu);
}
& a {
padding: 2px 0;
flex-grow: 1;
text-align: center;
text-decoration: none;
&:hover {
background: var(--color-background-hover-dropdown-menu);
outline: none;
}
&:focus {
border-radius: 4px;
/* Override the default focus style */
outline: 1px solid var(--color-outline-focus) !important;
outline-offset: -1px;
}
&:active {
background: var(--color-background-active-dropdown-menu);
}
}
}
.org-url {
margin-bottom: 7px;
}
.org-name,
.org-plan a {
color: var(--color-text-dropdown-menu);
}
.org-name {
font-size: 17px;
font-weight: 600;
line-height: 22px;
}
.org-plan,
.org-upgrade {
font-size: 14px;
}
.org-upgrade,
.org-url {
color: var(--color-gear-menu-lighter-text);
}
.org-upgrade a,
.org-version a {
color: var(--color-gear-menu-blue-text);
}
}
.light-theme,
.dark-theme {
display: none;
}
}
ul.navbar-dropdown-menu-outer-list {
@ -1203,6 +1293,10 @@ ul.navbar-dropdown-menu-outer-list {
&:last-child {
border-bottom: none;
& > ul {
padding-bottom: 0;
}
}
& > ul {
@ -1214,13 +1308,6 @@ ul.navbar-dropdown-menu-outer-list {
}
}
#gear-menu-dropdown {
.light-theme,
.dark-theme {
display: none;
}
}
.spectator-view {
#gear-menu-dropdown .dark-theme {
display: block;

View File

@ -1207,7 +1207,7 @@ $option_title_width: 180px;
font-size: 1.2em;
}
.invite-user-link i {
.invite-user-link .fa-user-plus {
text-decoration: none;
margin-right: 5px;
}

View File

@ -316,6 +316,15 @@ body {
0 12.177px 21.4737px 0 hsl(0deg 0% 0% / 9%),
0 18.7257px 35.4802px 0 hsl(0deg 0% 0% / 9%),
0 41px 80px 0 hsl(0deg 0% 0% / 13%);
--box-shadow-gear-menu: 0 2.7798px 4.1129px 0 hsl(0deg 0% 0% / 10%),
0 5.5113px 8.5783px 0 hsl(0deg 0% 0% / 9%),
0 8.4377px 13.9271px 0 hsl(0deg 0% 0% / 11%),
0 12.177px 21.4737px 0 hsl(0deg 0% 0% / 11%),
0 18.7257px 35.4802px 0 hsl(0deg 0% 0% / 15%),
0 41px 80px 0 hsl(0deg 0% 0% / 20%);
--color-navbar-icon: hsl(240deg 20% 50%);
--color-gear-menu-lighter-text: hsl(0deg 0% 40%);
--color-gear-menu-blue-text: hsl(217deg 100% 50%);
}
%dark-theme {
@ -464,6 +473,9 @@ body {
0 12.177px 21.4737px 0 hsl(0deg 0% 0% / 11%),
0 18.7257px 35.4802px 0 hsl(0deg 0% 0% / 15%),
0 41px 80px 0 hsl(0deg 0% 0% / 20%);
--color-navbar-icon: hsl(240deg 35% 60%);
--color-gear-menu-lighter-text: hsl(0deg 0% 50%);
--color-gear-menu-blue-text: hsl(217deg 100% 65%);
}
@media screen {
@ -766,59 +778,6 @@ p.n-margin {
}
}
#gear-menu .dropdown-menu {
.org-name,
.org-url {
padding: 0 20px;
}
.org-info {
text-align: center;
& a {
white-space: normal;
}
}
.org-name {
font-size: large;
font-weight: bold;
}
.org-url {
line-height: 100%;
color: hsl(0deg 0% 52%);
}
.org-version {
padding-top: 3px;
white-space: normal;
}
.org-upgrade {
color: hsl(226deg 82% 60%);
& a {
padding: 0;
}
}
.org-plan,
.org-upgrade {
& a {
line-height: 16px;
}
}
.plan-separator {
line-height: 8px;
}
.small-font-size {
font-size: 12px;
}
}
.recent_view_container #recent_view_table {
margin-top: var(--navbar-fixed-height);
}
@ -1177,10 +1136,6 @@ strong {
position: relative;
}
#gear-menu .light-theme {
display: none;
}
/* .dropdown-menu from v2.3.2
+ https://github.com/zulip/zulip/commit/7a3a3be7e547d3e8f0ed00820835104867f2433d
basic idea of this fix is to remove decorations from :hover and apply them only
@ -1266,29 +1221,6 @@ li.actual-dropdown-menu i {
margin-right: 3px;
}
#gear_menu_about_zulip {
.white_zulip_icon_without_text {
position: relative;
top: 3px;
width: 14px;
height: 14px;
margin-right: 3px;
filter: invert(20%) sepia(11%) saturate(0%) hue-rotate(272deg)
brightness(20%) contrast(95%);
}
.about_zulip_text {
position: relative;
top: 1.4px;
}
:hover {
.white_zulip_icon_without_text {
filter: none;
}
}
}
td.pointer {
vertical-align: top;
padding-top: 10px;
@ -2321,47 +2253,6 @@ div.focused-message-list {
}
}
#navbar-buttons {
white-space: nowrap;
display: inline-block;
float: right;
& ul.nav {
margin: 0;
.dropdown-toggle,
li.active .dropdown-toggle {
font-size: 20px;
color: inherit;
opacity: 0.5;
text-shadow: none;
padding-left: 0 !important;
background-color: inherit;
box-shadow: inherit;
display: block;
position: absolute;
right: 6px;
top: -3px;
}
.dropdown-toggle:hover,
li.active .dropdown-toggle:hover {
opacity: 1;
}
& li.dropdown.open .dropdown-toggle {
background: none;
opacity: 1;
text-shadow: none;
}
& li.dropdown li.divider {
margin-left: 15px;
margin-right: 15px;
}
}
}
#streamlist-toggle {
display: none;
position: absolute;
@ -2432,12 +2323,6 @@ div.focused-message-list {
}
}
#gear-menu .zulip-icon-language {
position: relative;
top: 2.5px;
left: -0.5px;
}
.nav .dropdown-menu a,
.typeahead.dropdown-menu a {
color: inherit;
@ -3091,9 +2976,9 @@ select.invite-as {
margin-right: 85px;
}
#navbar-buttons {
position: absolute;
right: 45px;
#gear-menu {
position: relative;
right: 40px;
}
}
@ -3244,8 +3129,7 @@ select.invite-as {
margin-right: 65px;
}
#navbar-buttons {
position: absolute;
#gear-menu {
right: 35px;
}
}
@ -3265,7 +3149,6 @@ select.invite-as {
}
#streamlist-toggle,
#navbar-buttons,
#message_view_header,
#searchbox,
.header {
@ -3287,11 +3170,6 @@ select.invite-as {
padding-bottom: 0;
}
#navbar-buttons ul.nav .dropdown-toggle,
#navbar-buttons ul.nav li.active .dropdown-toggle {
top: -8px;
}
.column-right #personal-menu .header-button-avatar {
width: 20px;
height: 20px;
@ -3511,8 +3389,7 @@ select.invite-as {
}
}
.header-button,
#navbar-buttons #settings-dropdown {
.header-button {
width: var(--header-height);
height: var(--header-height);
display: flex;
@ -3521,6 +3398,23 @@ select.invite-as {
position: relative;
top: 0;
right: 0;
&:hover,
&:focus {
text-decoration: none;
}
.zulip-icon {
color: var(--color-navbar-icon);
}
.zulip-icon-gear {
font-size: 18px;
}
.zulip-icon-triple-users {
font-size: 20px;
}
}
#personal-menu {
@ -3534,7 +3428,7 @@ select.invite-as {
}
}
.personal-menu-tippy {
.navbar-dropdown-tippy {
.tippy-box {
border: 0;
}

View File

@ -1,8 +0,0 @@
<ul class="nav" role="navigation">
<li class="dropdown actual-dropdown-menu" id="gear-menu">
<a id="settings-dropdown" tabindex="0" role="button" class="dropdown-toggle tippy-zulip-delayed-tooltip" data-target="nada" data-toggle="dropdown" data-tooltip-template-id="gear-menu-tooltip-template" aria-label="{{t 'Main menu'}}">
<i class="fa fa-cog settings-dropdown-cog" aria-hidden="true"></i>
</a>
{{> gear_menu_popover}}
</li>
</ul>

View File

@ -1,4 +1,4 @@
<ul class="navbar-dropdown-menu" id="gear-menu-dropdown" aria-labelledby="settings-dropdown">
<ul class="navbar-dropdown-menu" id="gear-menu-dropdown" aria-labelledby="settings-dropdown" data-simplebar>
<li class="org-info org-name">{{realm_name}}</li>
<li class="org-info org-url">{{realm_url}}</li>
{{#if is_self_hosted }}

View File

@ -51,7 +51,11 @@
<span id="userlist-toggle-unreadcount">0</span>
</a>
</div>
<div id="navbar-buttons" class="no-auto-hide-sidebar-overlays header-button" {{#if embedded}}class="hide-navbar-buttons-visibility"{{/if}}>
<div id="gear-menu" class="{{#if embedded}}hide-navbar-buttons-visibility{{/if}}">
<a id="settings-dropdown" tabindex="0" role="button" class="header-button tippy-zulip-delayed-tooltip" data-tooltip-template-id="gear-menu-tooltip-template">
<i class="zulip-icon zulip-icon-gear" aria-hidden="true"></i>
</a>
<span tabindex="0" class="focus-dropdown"></span>
</div>
<div id="personal-menu" class="hidden-for-spectators">
<a class="header-button tippy-zulip-delayed-tooltip" tabindex="0" role="button" data-tippy-content="{{t 'Personal menu' }}">

View File

@ -424,7 +424,7 @@ run_test("realm settings", ({override}) => {
override(settings_bots, "update_bot_permissions_ui", noop);
override(settings_invites, "update_invite_user_panel", noop);
override(sidebar_ui, "update_invite_user_option", noop);
override(gear_menu, "initialize", noop);
override(gear_menu, "rerender", noop);
override(narrow_title, "redraw_title", noop);
function test_electron_dispatch(event, fake_send_event) {

View File

@ -41,9 +41,7 @@ const emoji_picker = mock_esm("../src/emoji_picker", {
is_open: () => false,
toggle_emoji_popover() {},
});
const gear_menu = mock_esm("../src/gear_menu", {
is_open: () => false,
});
const gear_menu = mock_esm("../src/gear_menu");
const lightbox = mock_esm("../src/lightbox");
const list_util = mock_esm("../src/list_util");
const message_actions_popover = mock_esm("../src/message_actions_popover");
@ -329,7 +327,7 @@ run_test("basic mappings", () => {
assert_mapping("c", compose_actions, "start");
assert_mapping("x", compose_actions, "start");
assert_mapping("P", narrow, "by");
assert_mapping("g", gear_menu, "open");
assert_mapping("g", gear_menu, "toggle");
});
run_test("drafts open", ({override}) => {