Bug 1835687 - Use HTML search-bar widget in quick filter bar. r=aleca

Differential Revision: https://phabricator.services.mozilla.com/D190713

--HG--
rename : mail/components/unifiedtoolbar/content/search-bar.mjs => mail/base/content/widgets/search-bar.mjs
rename : mail/components/unifiedtoolbar/test/browser/browser_searchBar.js => mail/base/test/browser/browser_searchBar.js
rename : mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml => mail/base/test/browser/files/searchBar.xhtml
extra : amend_source : d2c17892f8717285cd887107bcb1428107e9d2dc
This commit is contained in:
Martin Giger 2023-10-25 17:41:17 +01:00
Родитель cf8cb52c4c
Коммит 451150f825
26 изменённых файлов: 268 добавлений и 120 удалений

Просмотреть файл

@ -2,18 +2,24 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include ./widgets/search-bar.inc.xhtml
<div id="quick-filter-bar" class="themeable-brighttext" hidden="hidden">
<div id="quickFilterBarContainer">
<button is="toggle-button" id="qfb-sticky"
class="button icon-button icon-only check-button"
data-l10n-id="quick-filter-bar-sticky">
</button>
<xul:search-textbox id="qfb-qs-textbox"
class="themeableSearchBox"
timeout="500"
maxlength="192"
data-l10n-id="quick-filter-bar-textbox"
data-l10n-attrs="placeholder" />
<search-bar id="qfb-qs-textbox"
maxlength="192"
data-l10n-id="quick-filter-bar-search"
data-l10n-attrs="label"
aria-keyshortcuts="Control+Shift+K">
<span slot="placeholder" data-l10n-id="quick-filter-bar-search-placeholder-with-key" class="kbd-container"></span>
<img data-l10n-id="quick-filter-bar-search-button"
slot="button"
class="qfb-search-button-icon"
src="" />
</search-bar>
<button id="qfd-dropdown"
class="button button-flat icon-button icon-only"
data-l10n-id="quick-filter-bar-dropdown">

Просмотреть файл

@ -15,6 +15,8 @@ XPCOMUtils.defineLazyModuleGetters(this, {
QuickFilterState: "resource:///modules/QuickFilterManager.jsm",
});
import("chrome://messenger/content/search-bar.mjs");
class ToggleButton extends HTMLButtonElement {
constructor() {
super();
@ -106,13 +108,13 @@ var quickFilterBar = {
if (!this.filterer.visible) {
this._showFilterBar(true);
}
document.getElementById(QuickFilterManager.textBoxDomId).select();
document.getElementById(QuickFilterManager.textBoxDomId).focus();
});
commandController.registerCallback("cmd_toggleQuickFilterBar", () => {
let show = !this.filterer.visible;
this._showFilterBar(show);
if (show) {
document.getElementById(QuickFilterManager.textBoxDomId).select();
document.getElementById(QuickFilterManager.textBoxDomId).focus();
}
});
window.addEventListener("keypress", event => {
@ -317,8 +319,12 @@ var quickFilterBar = {
}
};
}
if (domNode.namespaceURI == document.documentElement.namespaceURI) {
if (domNode.tagName === "search-bar") {
domNode.addEventListener("autocomplete", handlerDomId);
domNode.addEventListener("search", handlerDomId);
} else if (
domNode.namespaceURI == document.documentElement.namespaceURI
) {
domNode.addEventListener("click", handlerDomId);
} else {
domNode.addEventListener("command", handlerDomId);

Просмотреть файл

@ -0,0 +1,12 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
<template id="searchBarTemplate"
xmlns="http://www.w3.org/1999/xhtml">
<form>
<input type="search" placeholder="" required="required" />
<div aria-hidden="true"><slot name="placeholder"></slot></div>
<button class="button button-flat icon-button"><slot name="button"></slot></button>
</form>
</template>

Просмотреть файл

@ -4,18 +4,20 @@
/**
* Search input with customizable search button and placeholder.
* Attributes:
* - label: Search field label for accessibility tree.
* - disabled: When present, disable the search field and button.
* Slots in template (#searchBarTemplate):
* - placeholder: Content displayed as placeholder. When not provided, the value
* of the label attribute is shown as placeholder.
* - button: Content displayed on the search button.
* Template ID: #searchBarTemplate (from search-bar.inc.xhtml)
*
* @emits search: Event when a search should be executed. detail holds the
* search term.
* @emits autocomplte: Auto complete update. detail holds the current search
* term.
* @tagname search-bar
* @attribute {string} label - Search field label for accessibility tree.
* @attribute {boolean} disabled - When present, disable the search field and
* button.
* @attribute {number} maxlength - Max length of the input in the search field.
* @slot placeholder - Content displayed as placeholder. When not provided, the
* value of the label attribute is shown as placeholder.
* @slot button - Content displayed on the search button.
* @fires {CustomEvent} search - Event when a search should be executed. detail
* holds the search term.
* @fires {CustomEvent} autocomplete - Auto complete update. detail holds the
* current search term.
*/
export class SearchBar extends HTMLElement {
static get observedAttributes() {
@ -71,14 +73,16 @@ export class SearchBar extends HTMLElement {
this.#input = template.querySelector("input");
this.#button = template.querySelector("button");
template.querySelector("form").addEventListener("submit", this.#onSubmit, {
template.querySelector("form").addEventListener("submit", this, {
passive: false,
});
this.#input.setAttribute("aria-label", this.getAttribute("label"));
this.#input.setAttribute("maxlength", this.getAttribute("maxlength"));
template.querySelector("slot[name=placeholder]").textContent =
this.getAttribute("label");
this.#input.addEventListener("input", this.#onInput);
this.#input.addEventListener("input", this);
this.#input.addEventListener("keyup", this);
const styles = document.createElement("link");
styles.setAttribute("rel", "stylesheet");
@ -107,8 +111,30 @@ export class SearchBar extends HTMLElement {
}
}
handleEvent(event) {
switch (event.type) {
case "submit":
this.#onSubmit(event);
break;
case "input":
this.#onInput(event);
break;
case "keyup":
if (event.key === "Escape" && this.#input.value) {
this.reset();
this.#onInput();
event.preventDefault();
event.stopPropagation();
}
break;
}
}
focus() {
this.#input.focus();
if (this.#input.value) {
this.#input.select();
}
}
/**

Просмотреть файл

@ -103,6 +103,7 @@ messenger.jar:
content/messenger/header-fields.js (content/widgets/header-fields.js)
content/messenger/mailWidgets.js (content/widgets/mailWidgets.js)
content/messenger/pane-splitter.js (content/widgets/pane-splitter.js)
content/messenger/search-bar.mjs (content/widgets/search-bar.mjs)
content/messenger/statuspanel.js (content/widgets/statuspanel.js)
content/messenger/tabmail-tab.js (content/widgets/tabmail-tab.js)
content/messenger/tabmail-tabs.js (content/widgets/tabmail-tabs.js)

Просмотреть файл

@ -44,6 +44,7 @@ skip-if = os == 'mac'
[browser_paneFocus.js]
[browser_paneSplitter.js]
[browser_preferDisplayName.js]
[browser_searchBar.js]
[browser_searchMessages.js]
[browser_spacesToolbar.js]
[browser_spacesToolbarCustomize.js]

Просмотреть файл

@ -74,8 +74,8 @@ function is_visible(element) {
}
add_setup(async function () {
let tab = tabmail.openTab("contentTab", {
url: "chrome://mochitests/content/browser/comm/mail/components/unifiedtoolbar/test/browser/files/searchBar.xhtml",
const tab = tabmail.openTab("contentTab", {
url: "chrome://mochitests/content/browser/comm/mail/base/test/browser/files/searchBar.xhtml",
});
await BrowserTestUtils.browserLoaded(tab.browser);
@ -112,6 +112,20 @@ add_task(async function test_focus() {
input,
"Input is focused when search bar is focused"
);
input.blur();
input.value = "foo";
searchBar.focus();
is(
searchBar.shadowRoot.activeElement,
input,
"Input is focused when search bar is focused"
);
is(input.selectionStart, 0, "Selection at the beginning");
is(input.selectionEnd, 3, "Selection to the end");
searchBar.reset();
});
add_task(async function test_autocompleteEvent() {
@ -261,3 +275,17 @@ add_task(async function test_disabled() {
ok(!input.disabled, "Input enabled again");
ok(!button.disabled, "Button enabled again");
});
add_task(async function test_clearWithEscape() {
const input = searchBar.shadowRoot.querySelector("input");
searchBar.focus();
input.value = "foo bar";
const eventPromise = BrowserTestUtils.waitForEvent(searchBar, "autocomplete");
await BrowserTestUtils.synthesizeKey("KEY_Escape", {}, browser);
const event = await eventPromise;
is(event.detail, "", "Autocomplete event with empty value");
is(input.value, "", "Input was cleared");
});

Просмотреть файл

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8" />
<title>Search bar element test</title>
<script type="module" src="chrome://messenger/content/unifiedtoolbar/search-bar.mjs"></script>
<script type="module" src="chrome://messenger/content/search-bar.mjs"></script>
</head>
<body>
<template id="searchBarTemplate">

Просмотреть файл

@ -46,3 +46,12 @@ window.MozXULElement = {
// Enable props tables documentation.
setCustomElementsManifest(customElementsManifest);
// Stop typing on a keyboard inside the sandbox from messing with the storybook
// UI
const stopEvent = event => {
event.stopPropagation();
};
document.addEventListener("keydown", stopEvent);
document.addEventListener("keypress", stopEvent);
document.addEventListener("keyup", stopEvent);

Просмотреть файл

@ -4,14 +4,25 @@
import { html } from "lit";
import { action } from "@storybook/addon-actions";
import "mail/components/unifiedtoolbar/content/search-bar.mjs"; //eslint-disable-line import/no-unassigned-import
/* eslint-disable import/no-unassigned-import */
import "mail/base/content/widgets/search-bar.mjs";
import "mail/themes/shared/mail/colors.css";
import "mail/themes/shared/mail/layout.css";
import "mail/themes/shared/mail/widgets.css";
/* eslint-enable import/no-unassigned-import */
export default {
title: "Widgets/Search Bar",
component: "search-bar",
argTypes: {
disabled: {
control: "boolean",
},
},
};
export const SearchBar = () => html`
const Template = ({ label, disabled }) => html`
<!-- #include mail/base/content/widgets/search-bar.inc.xhtml -->
<template id="searchBarTemplate">
<form>
<input type="search" placeholder="" required="required" />
@ -24,8 +35,10 @@ export const SearchBar = () => html`
<search-bar
@search="${action("search")}"
@autocomplete="${action("autocomplete")}"
label="${label}"
?disabled="${disabled}"
>
<span slot="placeholder"
<span slot="placeholder" class="kbd-container"
>Search Field Placeholder <kbd>Ctrl</kbd> + <kbd>K</kbd>
</span>
<img
@ -36,3 +49,8 @@ export const SearchBar = () => html`
/>
</search-bar>
`;
export const SearchBar = Template.bind({});
SearchBar.args = {
label: "",
disabled: false,
};

Просмотреть файл

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
import { SearchBar } from "chrome://messenger/content/unifiedtoolbar/search-bar.mjs";
import { SearchBar } from "chrome://messenger/content/search-bar.mjs";
const { XPCOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/XPCOMUtils.sys.mjs"
@ -101,24 +101,49 @@ class GlobalSearchBar extends SearchBar {
// Need to call this after the shadow root test, since this will always set
// up a shadow root.
super.connectedCallback();
this.addEventListener("search", this.#handleSearch);
this.addEventListener("autocomplete", this.#handleAutocomplete);
this.addEventListener("search", this);
this.addEventListener("autocomplete", this);
// Capturing to avoid the default cursor movements inside the input.
this.addEventListener("keydown", this.#handleKeydown, {
this.addEventListener("keydown", this, {
capture: true,
});
this.addEventListener("focus", this.#handleFocus);
this.addEventListener("focus", this);
this.addEventListener("blur", this);
this.addEventListener("drop", this.#handleDrop, { capture: true });
this.addEventListener("drop", this, { capture: true });
}
#popupWasOpenAtDown = false;
handleEvent(event) {
switch (event.type) {
case "search":
this.#handleSearch(event);
break;
case "autocomplete":
this.#handleAutocomplete(event);
break;
case "keydown":
this.#handleKeydown(event);
this.#popupWasOpenAtDown = this.popup.mPopupOpen;
break;
case "focus":
this.#handleFocus(event);
break;
case "drop":
this.#handleDrop(event);
break;
case "keyup":
if (!this.#popupWasOpenAtDown || event.key !== "Escape") {
super.handleEvent(event);
}
break;
case "blur":
if (this.popup.mPopupOpen) {
this.popup.closePopup();
}
break;
default:
super.handleEvent(event);
}
}

Просмотреть файл

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
import "./search-bar.mjs"; // eslint-disable-line import/no-unassigned-import
import "chrome://messenger/content/search-bar.mjs"; // eslint-disable-line import/no-unassigned-import
import "./customization-palette.mjs"; // eslint-disable-line import/no-unassigned-import
import "./customization-target.mjs"; // eslint-disable-line import/no-unassigned-import
import {

Просмотреть файл

@ -96,10 +96,22 @@ class UnifiedToolbarCustomization extends HTMLElement {
this.initialize();
this.append(template);
this.#updateResetToDefault();
this.addEventListener("keyup", this.#handleKeyboard);
this.addEventListener("keyup", this.#closeByKeyboard);
this.addEventListener("keypress", this.#handleKeyboard);
this.addEventListener("keydown", this.#handleKeyboard);
this.addEventListener("keyup", this);
this.addEventListener("keypress", this);
this.addEventListener("keydown", this);
}
handleEvent(event) {
switch (event.type) {
case "keyup":
this.#handleKeyboard(event);
this.#closeByKeyboard(event);
break;
case "keypress":
case "keydown":
this.#handleKeyboard(event);
break;
}
}
#handleItemChange = event => {

Просмотреть файл

@ -6,7 +6,7 @@
<global-search-bar data-l10n-id="search-bar-item"
data-l10n-attrs="label"
aria-keyshortcuts="Control+K">
<span slot="placeholder" data-l10n-id="search-bar-placeholder-with-key2"></span>
<span slot="placeholder" data-l10n-id="search-bar-placeholder-with-key2" class="kbd-container"></span>
<img data-l10n-id="search-bar-button"
slot="button"
class="search-button-icon"

Просмотреть файл

@ -3,15 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#include ./unifiedToolbarCustomizableItems.inc.xhtml
<html:template id="searchBarTemplate"
xmlns="http://www.w3.org/1999/xhtml">
<form>
<input type="search" placeholder="" required="required" />
<div aria-hidden="true"><slot name="placeholder"></slot></div>
<button class="button button-flat icon-button"><slot name="button"></slot></button>
</form>
</html:template>
#include ../../../base/content/widgets/search-bar.inc.xhtml
<html:template id="unifiedToolbarTemplate">
# Required for placing the window controls in the proper place without having

Просмотреть файл

@ -20,7 +20,6 @@ messenger.jar:
content/messenger/unifiedtoolbar/extension-action-button.mjs (content/extension-action-button.mjs)
content/messenger/unifiedtoolbar/list-box-selection.mjs (content/list-box-selection.mjs)
content/messenger/unifiedtoolbar/mail-tab-button.mjs (content/mail-tab-button.mjs)
content/messenger/unifiedtoolbar/search-bar.mjs (content/search-bar.mjs)
content/messenger/unifiedtoolbar/unified-toolbar.mjs (content/unified-toolbar.mjs)
content/messenger/unifiedtoolbar/unified-toolbar-button.mjs (content/unified-toolbar-button.mjs)
content/messenger/unifiedtoolbar/unified-toolbar-customization.mjs (content/unified-toolbar-customization.mjs)

Просмотреть файл

@ -11,7 +11,6 @@ subsuite = thunderbird
support-files = files/**
[browser_customizableItems.js]
[browser_searchBar.js]
[browser_toolbarMigration.js]
[browser_unifiedToolbar.js]
[browser_unifiedToolbarCustomization.js]

Просмотреть файл

@ -122,20 +122,26 @@ quick-filter-bar-results =
*[other] { $count } messages
}
quick-filter-bar-search =
.label = Filter messages:
# Keyboard shortcut for the text search box.
# This should match quick-filter-bar-show in messenger.ftl.
quick-filter-bar-textbox-shortcut =
{ PLATFORM() ->
[macos] ⇧ ⌘ K
*[other] Ctrl+Shift+K
}
quick-filter-bar-search-shortcut = {
PLATFORM() ->
[macos] <kbd></kbd> <kbd></kbd> <kbd>K</kbd>
*[other] <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>K</kbd>
}
# This is the empty text for the text search box.
# The goal is to convey to the user that typing in the box will filter
# the messages and that there is a hotkey they can press to get to the
# box faster.
quick-filter-bar-textbox =
.placeholder = Filter these messages <{ quick-filter-bar-textbox-shortcut }>
# The goal is to convey to the user that typing in the box will filter the
# messages and that there is a hotkey they can press to get to the box faster.
quick-filter-bar-search-placeholder-with-key = Filter messages… { quick-filter-bar-search-shortcut }
# Label of the search button in the quick filter bar text box. Clicking it will
# launch a global search.
quick-filter-bar-search-button =
.alt = Search everywhere
# Tooltip of the Any-of/All-of tagging mode selector.
quick-filter-bar-boolean-mode =

Просмотреть файл

@ -466,7 +466,7 @@ quick-filter-bar-toggle =
.accesskey = Q
# This is the key used to show the quick filter bar.
# This should match quick-filter-bar-textbox-shortcut in about3Pane.ftl.
# This should match quick-filter-bar-search-shortcut in about3Pane.ftl.
quick-filter-bar-show =
.key = k

Просмотреть файл

@ -1169,24 +1169,24 @@ var MessageTextFilter = {
},
onCommand(aState, aNode, aEvent, aDocument) {
let text = aNode.value.length ? aNode.value : null;
if (text == aState.text) {
let text = aEvent.detail || null;
const isSearch = aEvent.type === "search";
if (isSearch) {
let upsell = aDocument.getElementById("qfb-text-search-upsell");
if (upsell.state == "open") {
upsell.hidePopup();
let tabmail =
aDocument.ownerGlobal.top.document.getElementById("tabmail");
tabmail.openTab("glodaFacet", {
searcher: new lazy.GlodaMsgSearcher(null, aState.text),
});
}
return [aState, false];
let tabmail =
aDocument.ownerGlobal.top.document.getElementById("tabmail");
tabmail.openTab("glodaFacet", {
searcher: new lazy.GlodaMsgSearcher(null, aState.text),
});
aEvent.preventDefault();
}
aState.text = text;
aDocument.getElementById("quick-filter-bar-filter-text-bar").hidden =
text == null;
return [aState, true];
aDocument.getElementById("quick-filter-bar-filter-text-bar").hidden = !text;
return [aState, !isSearch];
},
reflectInDOM(aNode, aFilterValue, aDocument, aMuxer, aFromPFP) {
@ -1228,11 +1228,10 @@ var MessageTextFilter = {
panel.hidePopup();
}
// Update the text if it has changed (linux does weird things with empty
// text if we're transitioning emptytext to emptytext).
// Propagate a cleared text filter to the search bar input.
let desiredValue = aFilterValue.text || "";
if (aNode.value != desiredValue && aNode != aMuxer.activeElement) {
aNode.value = desiredValue;
if (!desiredValue) {
aNode.reset();
}
// Update our expanded filters buttons.

Просмотреть файл

@ -134,7 +134,6 @@ add_task(async function test_escape_rules() {
add_task(async function test_control_shift_k_shows_quick_filter_bar() {
let about3Pane = get_about_3pane();
let dispatcha = document.commandDispatcher;
let qfbTextbox = about3Pane.document.getElementById("qfb-qs-textbox");
// focus explicitly on the thread pane so we know where the focus is.
@ -144,37 +143,42 @@ add_task(async function test_control_shift_k_shows_quick_filter_bar() {
// hit control-shift-k to get in the quick filter box
EventUtils.synthesizeKey("k", { accelKey: true, shiftKey: true });
if (dispatcha.focusedElement != qfbTextbox.inputField) {
throw new Error("control-shift-k did not focus quick filter textbox");
}
Assert.strictEqual(
about3Pane.document.activeElement,
qfbTextbox,
"control-shift-k did not focus quick filter textbox"
);
await set_filter_text("search string");
// hit control-shift-k to select the text in the quick filter box
EventUtils.synthesizeKey("k", { accelKey: true, shiftKey: true });
if (dispatcha.focusedElement != qfbTextbox.inputField) {
throw new Error(
"second control-shift-k did not keep focus on filter textbox"
);
}
if (
qfbTextbox.inputField.selectionStart != 0 ||
qfbTextbox.inputField.selectionEnd != qfbTextbox.inputField.textLength
) {
throw new Error(
"second control-shift-k did not select text in filter textbox"
);
}
Assert.strictEqual(
about3Pane.document.activeElement,
qfbTextbox,
"second control-shift-k did not keep focus on filter textbox"
);
const input = qfbTextbox.shadowRoot.querySelector("input");
Assert.equal(
input.selectionStart,
0,
"Selection starts at the beginning of the input"
);
Assert.equal(
input.selectionEnd,
"search string".length,
"Selection ends at the end of the input"
);
// hit escape and make sure the text is cleared, but the quick filter bar is
// still open.
EventUtils.synthesizeKey("VK_ESCAPE", {});
EventUtils.synthesizeKey("KEY_Escape", {});
assert_quick_filter_bar_visible(true);
assert_filter_text("");
// hit escape one more time and make sure we finally collapsed the quick
// filter bar.
EventUtils.synthesizeKey("VK_ESCAPE", {});
EventUtils.synthesizeKey("KEY_Escape", {});
assert_quick_filter_bar_visible(false);
teardownTest();
});

Просмотреть файл

@ -333,14 +333,18 @@ function assert_text_constraints_checked(...aArgs) {
async function set_filter_text(aText) {
// We're not testing the reliability of the textbox widget; just poke our text
// in and trigger the command logic.
let textbox = about3Pane.document.getElementById("qfb-qs-textbox");
let textbox = about3Pane.document
.getElementById("qfb-qs-textbox")
.shadowRoot.querySelector("input");
textbox.value = aText;
textbox.doCommand();
textbox.dispatchEvent(new Event("input"));
await wait_for_all_messages_to_load(mc);
}
function assert_filter_text(aText) {
let textbox = get_about_3pane().document.getElementById("qfb-qs-textbox");
let textbox = get_about_3pane()
.document.getElementById("qfb-qs-textbox")
.shadowRoot.querySelector("input");
if (textbox.value != aText) {
throw new Error(
"Expected text filter value of '" +

Просмотреть файл

@ -61,12 +61,14 @@
#qfb-qs-textbox {
flex: 1;
height: unset;
margin: 3px;
padding-block: 3px;
max-width: 450px;
}
.qfb-search-button-icon {
content: var(--icon-search);
}
@container threadPane (max-width: 499px) {
#qfb-qs-textbox {
min-width: 200px;

Просмотреть файл

@ -12,7 +12,6 @@ form {
position: relative;
min-height: max(1.2em, calc(1.2em + 2 * var(--padding-block)));
height: 100%;
max-height: 2em;
}
input {

Просмотреть файл

@ -167,23 +167,6 @@ global-search-bar:not([hidden]):-moz-lwtheme {
text-shadow: none;
}
kbd {
background-color: var(--layout-background-3);
color: var(--layout-color-2);
text-transform: uppercase;
font-size: 0.8rem;
line-height: 1;
font-weight: bold;
box-shadow: inset 0px -1px 0px var(--layout-border-2);
border-radius: 3px;
display: inline-block;
padding: 2px 4px;
}
kbd:first-of-type {
margin-inline-start: 6px;
}
span[slot="placeholder"] {
display: flex;
align-items: center;

Просмотреть файл

@ -396,3 +396,20 @@
}
}
}
.kbd-container kbd {
background-color: var(--layout-background-3);
color: var(--layout-color-2);
text-transform: uppercase;
font-size: 0.8rem;
line-height: 1;
font-weight: bold;
box-shadow: inset 0px -1px 0px var(--layout-border-2);
border-radius: 3px;
display: inline-block;
padding: 2px 4px;
&:first-of-type {
margin-inline-start: 6px;
}
}