[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:
@@ -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');
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"]
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user