[IMP] web, mail: Command palette

The purpose of this commit is to:
- Improve the design of the command palette
- Add few data-command-category and data-hotkey
- Scrolling through the command palette dropdown with the up/down arrows
should loop
- Moving the mouse over the palette command no longer causes scrolling

Task-2590390
This commit is contained in:
Francois (fge)
2021-07-13 07:32:09 +00:00
parent ae7e5960f1
commit e95080792a
16 changed files with 121 additions and 62 deletions
+1
View File
@@ -200,6 +200,7 @@
'mail/static/src/utils/throttle/throttle.js',
'mail/static/src/utils/timer/timer.js',
'mail/static/src/utils/utils.js',
'mail/static/src/webclient/commands/command_category.js',
'mail/static/src/widgets/discuss/discuss.js',
'mail/static/src/widgets/discuss_invite_partner_dialog/discuss_invite_partner_dialog.js',
'mail/static/src/widgets/form_renderer/form_renderer.js',
@@ -2,7 +2,7 @@
<templates xml:space="preserve">
<t t-name="mail.ChatterContainer" owl="1">
<div class="o_ChatterContainer">
<div class="o_ChatterContainer" data-command-category="mail">
<t t-if="chatter">
<Chatter chatterLocalId="chatter.localId"/>
</t>
@@ -13,6 +13,8 @@
'o-bordered': chatter.hasExternalBorder,
}"
t-att-disabled="chatter.isDisabled"
title="Send a message"
data-hotkey="m"
t-on-click="_onClickSendMessage"
>
Send message
@@ -25,12 +27,14 @@
}"
t-att-disabled="chatter.isDisabled"
t-on-click="_onClickLogNote"
title="Log a note"
data-hotkey="shift+m"
>
Log note
</button>
</t>
<t t-if="chatter.hasActivities">
<button class="btn btn-link o_ChatterTopbar_button o_ChatterTopbar_buttonScheduleActivity" type="button" t-att-disabled="chatter.isDisabled" t-on-click="_onClickScheduleActivity">
<button class="btn btn-link o_ChatterTopbar_button o_ChatterTopbar_buttonScheduleActivity" type="button" t-att-disabled="chatter.isDisabled" t-on-click="_onClickScheduleActivity" title="Schedule an activity" data-hotkey="shift+a">
<i class="fa fa-clock-o"/>
Schedule activity
</button>
@@ -37,7 +37,7 @@
* As a result, Min() is ignored by SCSS while CSS interprets as its min() function.
*/
max-height: Min(calc(100vh - 140px), 630px);
z-index: 1100; // on top of chat windows
z-index: 1050; // on top of chat windows
}
&.o-mobile {
@@ -0,0 +1,7 @@
/** @odoo-module **/
import { registry } from "@web/core/registry";
import { _lt } from "@web/core/l10n/translation";
const commandCategoryRegistry = registry.category("command_categories");
commandCategoryRegistry.add("mail", { label: _lt("Discuss") }, { sequence: 20 });
@@ -184,6 +184,8 @@ odoo.define('web.DropdownMenu', function (require) {
},
title: { type: String, optional: 1 },
closeOnSelected: { type: Boolean, optional: 1 },
hotkey: { type: String, optional: 1 },
hotkeyTitle: { type: String, optional: 1 },
};
DropdownMenu.template = 'web.DropdownMenu';
@@ -1139,7 +1139,7 @@ var FormRenderer = BasicRenderer.extend({
this.$el.toggleClass('o_form_nosheet', !this.has_sheet);
if (this.has_sheet) {
this.$el.children().not('.o_FormRenderer_chatterContainer')
.wrapAll($('<div/>', {class: 'o_form_sheet_bg'}));
.wrapAll($('<div/>', {class: 'o_form_sheet_bg', 'data-command-category': "app"}));
}
this.$el.toggleClass('o_form_editable', this.mode === 'edit');
this.$el.toggleClass('o_form_readonly', this.mode === 'readonly');
+19 -12
View File
@@ -4,7 +4,7 @@
<!-- Owl Templates -->
<t t-name="web.ControlPanel" owl="1">
<div class="o_control_panel">
<div class="o_control_panel" data-command-category="actions">
<div class="o_cp_top">
<div class="o_cp_top_left">
<ol t-if="props.withBreadcrumbs" class="breadcrumb" role="navigation">
@@ -13,6 +13,7 @@
t-att-class="{ o_back_button: bc_index === props.breadcrumbs.length - 1 }"
t-att-accesskey="bc_last and 'b'"
t-on-click.prevent="trigger('breadcrumb-clicked', { controllerID: bc.controllerID })"
title="Previous menu"
>
<a t-if="bc.title" href="#" t-esc="bc.title"/>
<em t-else="" class="text-warning">Unnamed</em>
@@ -159,6 +160,8 @@
tabindex="-1"
t-on-click="state.open = !state.open"
t-on-keydown="_onButtonKeydown"
t-att-title="props.hotkeyTitle"
t-att-data-hotkey="props.hotkey"
>
<i t-if="icon" t-att-class="icon + ' small mr-1'"/>
<span class="o_dropdown_title" t-esc="title"/>
@@ -545,8 +548,8 @@
class="fa fa-chevron-left btn btn-secondary o_pager_previous px-2 rounded-left"
t-att-disabled="state.disabled || singlePage"
t-att-accesskey="props.withAccessKey ? 'p' : false"
aria-label="Previous"
title="Previous"
aria-label="Previous page"
title="Previous page"
tabindex="-1"
t-on-click="_changeSelection(-1)"
/>
@@ -554,8 +557,8 @@
class="fa fa-chevron-right btn btn-secondary o_pager_next px-2 rounded-right"
t-att-disabled="state.disabled || singlePage"
t-att-accesskey="props.withAccessKey ? 'n' : false"
aria-label="Next"
title="Next"
aria-label="Next page"
title="Next page"
tabindex="-1"
t-on-click="_changeSelection(1)"
/>
@@ -632,12 +635,16 @@
title="env._t('Print')"
items="printItems"
icon="'fa fa-print'"
hotkey="'shift+u'"
hotkeyTitle="'Printing options'"
/>
<DropdownMenu t-if="actionItems.length"
title="env._t('Action')"
items="actionItems"
icon="'fa fa-cog'"
closeOnSelected="true"
hotkey="'u'"
hotkeyTitle="'Additional actions'"
/>
</div>
</t>
@@ -824,14 +831,14 @@
<t t-name="ListView.buttons">
<div class="o_list_buttons d-flex" role="toolbar" aria-label="Main actions">
<button type="button" class="btn btn-primary o_list_button_save" accesskey="s">
<button type="button" class="btn btn-primary o_list_button_save" title="Save record" accesskey="s">
Save
</button>
<button type="button" class="btn btn-secondary o_list_button_discard" accesskey="j">
<button type="button" class="btn btn-secondary o_list_button_discard" title="Discard record" accesskey="j">
Discard
</button>
<t t-if="widget.is_action_enabled('create')">
<button type="button" class="btn btn-primary o_list_button_add" accesskey="c">
<button type="button" class="btn btn-primary o_list_button_add" title="Create record" accesskey="c">
Create
</button>
</t>
@@ -887,21 +894,21 @@
<t t-name="FormView.buttons">
<div class="o_form_buttons_view" role="toolbar" aria-label="Main actions">
<button t-if="widget.is_action_enabled('edit')" type="button"
class="btn btn-primary o_form_button_edit" accesskey="a">
class="btn btn-primary o_form_button_edit" title="Edit record" accesskey="a">
Edit
</button>
<button t-if="widget.is_action_enabled('create')" type="button"
class="btn btn-secondary o_form_button_create" accesskey="c">
class="btn btn-secondary o_form_button_create" title="Create record" accesskey="c">
Create
</button>
</div>
<div class="o_form_buttons_edit" role="toolbar" aria-label="Main actions">
<button type="button"
class="btn btn-primary o_form_button_save" accesskey="s">
class="btn btn-primary o_form_button_save" title="Save record" accesskey="s">
Save
</button>
<button type="button"
class="btn btn-secondary o_form_button_cancel" accesskey="j">
class="btn btn-secondary o_form_button_cancel" title="Discard record" accesskey="j">
Discard
</button>
</div>
+1 -1
View File
@@ -2,7 +2,7 @@
<t t-name="KanbanView.buttons">
<div>
<button t-if="!noCreate" type="button" t-attf-class="btn #{btnClass} o-kanban-button-new" accesskey="c">
<button t-if="!noCreate" type="button" t-attf-class="btn #{btnClass} o-kanban-button-new" title="Create record" accesskey="c">
<t t-esc="create_text || _t('Create')"/>
</button>
</div>
@@ -5,7 +5,6 @@ import { registry } from "../../core/registry";
const commandCategoryRegistry = registry.category("command_categories");
commandCategoryRegistry.add("app", { label: _lt("Current App Commands") }, { sequence: 10 });
commandCategoryRegistry.add("mail", { label: _lt("Discuss") }, { sequence: 20 });
commandCategoryRegistry.add("actions", { label: _lt("More Actions") }, { sequence: 30 });
commandCategoryRegistry.add("navbar", { label: _lt("NavBar") }, { sequence: 40 });
commandCategoryRegistry.add("default", { label: _lt("Other commands") }, { sequence: 100 });
@@ -3,7 +3,6 @@
import { registry } from "@web/core/registry";
import { useAutofocus } from "../../core/autofocus_hook";
import { isMacOS } from "../../core/browser/feature_detection";
import { useEffect } from "../../core/effect_hook";
import { useHotkey } from "../../core/hotkey_hook";
import { scrollTo } from "../../core/utils/scrolling";
@@ -53,37 +52,9 @@ export class CommandPalette extends Component {
useAutofocus();
this.mouseSelectionActive = false;
useEffect(() => {
const index = this.state.commands.indexOf(this.state.selectedCommand);
const listbox = this.el.querySelector(".o_command_palette_listbox");
const command = listbox.querySelector(`#o_command_${index}`);
scrollTo(command, listbox);
});
useHotkey("Enter", () => {
this.executeSelectedCommand();
});
useHotkey(
"ArrowUp",
() => {
this.mouseSelectionActive = false;
const index = this.state.commands.indexOf(this.state.selectedCommand);
if (index > 0) {
this.selectCommand(index - 1);
}
},
{ allowRepeat: true }
);
useHotkey(
"ArrowDown",
() => {
this.mouseSelectionActive = false;
const index = this.state.commands.indexOf(this.state.selectedCommand);
if (index < this.state.commands.length - 1) {
this.selectCommand(index + 1);
}
},
{ allowRepeat: true }
);
useHotkey("Enter", () => this.executeSelectedCommand());
useHotkey("ArrowUp", () => this.selectCommandAndScrollTo("PREV"), { allowRepeat: true });
useHotkey("ArrowDown", () => this.selectCommandAndScrollTo("NEXT"), { allowRepeat: true });
for (const command of this.initialCommands) {
if (command.hotkey) {
@@ -128,6 +99,24 @@ export class CommandPalette extends Component {
this.state.selectedCommand = this.state.commands[index];
}
selectCommandAndScrollTo(type) {
// In case the mouse is on the palette command, it avoids the selection
// of a command caused by a scroll.
this.mouseSelectionActive = false;
const index = this.state.commands.indexOf(this.state.selectedCommand);
let nextIndex;
if (type === "NEXT") {
nextIndex = index < this.state.commands.length - 1 ? index + 1 : 0;
} else if (type === "PREV") {
nextIndex = index > 0 ? index - 1 : this.state.commands.length - 1;
}
this.selectCommand(nextIndex);
const listbox = this.el.querySelector(".o_command_palette_listbox");
const command = listbox.querySelector(`#o_command_${nextIndex}`);
scrollTo(command, listbox);
}
onCommandClicked(index) {
this.selectCommand(index);
this.executeSelectedCommand();
@@ -16,7 +16,8 @@
justify-content: space-between;
background-color: white;
border-bottom: 1px solid #ccc;
padding: 0.5rem 1em;
padding: 0.8rem 1em;
font-size: 1.5rem;
input {
border: none;
@@ -37,8 +38,9 @@
}
&_empty {
padding: 0.7rem 1em;
padding: 1rem 1.3em;
line-height: 1.8em;
font-style: italic;
}
.o_command_category {
@@ -53,18 +55,19 @@
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.2rem 1em;
padding: 0.5rem 1.3em;
position: relative;
line-height: 1.8em;
height: 2.8em;
&.focused {
background: $o-gray-200;
background: #00a09d26;
}
kbd {
background-color: #f4f7f8;
border-radius: 3px;
border: 1px solid #b4b4b4;
border: 1px solid #e5e5e5;
$shadowvalue: 0px 1px 1px rgba(0, 0, 0, 0.2),
inset 0px -1px 1px 1px rgba(230, 230, 230, 0.8),
inset 0px 2px 0px 0px rgba(255, 255, 255, 0.8);
@@ -14,7 +14,7 @@
<t t-foreach="categories" t-as="category">
<div class="o_command_category">
<div class="o_command_category" t-att-aria-label="category.label">
<t t-foreach="category.commands" t-as="command">
<t t-set="commandIndex" t-value="state.commands.indexOf(command)" />
@@ -1,6 +1,7 @@
/** @odoo-module **/
import { registry } from "../../core/registry";
import { capitalize } from "../../core/utils/strings";
import { CommandPaletteDialog } from "./command_palette_dialog";
/**
@@ -47,12 +48,13 @@ export const commandService = {
const description =
el.title ||
el.dataset.originalTitle || // LEGACY: bootstrap moves title to data-original-title
el.placeholder ||
(el.innerText &&
`${el.innerText.slice(0, 50)}${el.innerText.length > 50 ? "..." : ""}`) ||
"no description provided";
commands.push({
name: description,
name: capitalize(description.trim().toLowerCase()),
hotkey: `${overlayModifier}+${el.dataset.hotkey}`,
action: () => {
// AAB: not sure it is enough, we might need to trigger all events that occur when you actually click
@@ -153,7 +153,7 @@ QUnit.test("data-hotkey added to command palette", async (assert) => {
assert.containsN(target, ".o_command", 2);
assert.deepEqual(
[...target.querySelectorAll(".o_command span:first-child")].map((el) => el.textContent),
["Aria Stark", "Bran Stark"]
["Aria stark", "Bran stark"]
);
// Click on first command
@@ -296,6 +296,51 @@ QUnit.test("can be searched", async (assert) => {
);
});
QUnit.test("navigate in the command palette with the arrows", async (assert) => {
assert.expect(6);
testComponent = await mount(TestComponent, { env, target });
// Register some commands
function action() {}
const names = ["Cersei", "Jaime", "Tyrion"];
for (const name of names) {
env.services.command.add(name, action);
}
// Open palette
triggerHotkey("control+k");
await nextTick();
let focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, names[0]);
triggerHotkey("arrowdown");
await nextTick();
focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, names[1]);
triggerHotkey("arrowdown");
await nextTick();
focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, names[2]);
triggerHotkey("arrowdown");
await nextTick();
focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, names[0]);
triggerHotkey("arrowup");
await nextTick();
focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, names[2]);
triggerHotkey("arrowup");
await nextTick();
focusedCommand = target.querySelector(".o_command.focused");
assert.strictEqual(focusedCommand.textContent, names[1]);
});
QUnit.test("command categories", async (assert) => {
testComponent = await mount(TestComponent, { env, target });
@@ -348,7 +393,7 @@ QUnit.test("data-command-category", async (assert) => {
".o_command_category:nth-of-type(1) .o_command > span:first-child"
),
].map((el) => el.textContent),
["Robert Baratheon", "Joffrey Baratheon"]
["Robert baratheon", "Joffrey baratheon"]
);
assert.deepEqual(
[
@@ -356,7 +401,7 @@ QUnit.test("data-command-category", async (assert) => {
".o_command_category:nth-of-type(2) .o_command > span:first-child"
),
].map((el) => el.textContent),
["Aria Stark", "Bran Stark"]
["Aria stark", "Bran stark"]
);
});
+2 -2
View File
@@ -9,8 +9,8 @@
<form class="ml-auto">
<!-- Uncomment the following line when the mobile preview will be available. -->
<!-- <button type="button" class="btn btn-secondary" name="mobile" data-action="mobilePreview"><i class="fa fa-mobile"></i></button> -->
<button type="button" class="btn btn-secondary" data-action="cancel" accesskey="j">Discard</button>
<button type="button" class="btn btn-primary" data-action="save" accesskey="s">Save</button>
<button type="button" class="btn btn-secondary" data-action="cancel" title="Discard record" accesskey="j">Discard</button>
<button type="button" class="btn btn-primary" data-action="save" title="Save record" accesskey="s">Save</button>
</form>
</div>
<div id="snippets_menu">