mirror of
https://github.com/livebook-dev/livebook.git
synced 2025-11-08 05:04:46 +08:00
Usability improvements for custom keyboard controls (#2145)
Co-authored-by: José Valim <jose.valim@gmail.com>
This commit is contained in:
parent
cd6239b383
commit
005a9e86f7
5 changed files with 105 additions and 31 deletions
|
|
@ -1,14 +1,20 @@
|
||||||
import { getAttributeOrThrow, parseBoolean } from "../lib/attribute";
|
import { getAttributeOrThrow, parseBoolean } from "../lib/attribute";
|
||||||
import { cancelEvent, isEditableElement } from "../lib/utils";
|
import { cancelEvent, isEditableElement, isMacOS } from "../lib/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A hook for ControlComponent to handle user keyboard interactions.
|
* A hook for ControlComponent to handle user keyboard interactions.
|
||||||
*
|
*
|
||||||
* ## Configuration
|
* ## Configuration
|
||||||
*
|
*
|
||||||
* * `data-keydown-enabled` - whether keydown events should be intercepted
|
* * `data-cell-id` - id of the cell in which the control is rendered
|
||||||
*
|
*
|
||||||
* * `data-keyup-enabled` - whether keyup events should be intercepted
|
* * `data-default-handlers` - whether keyboard events should be
|
||||||
|
* intercepted and canceled, disabling session shortcuts. Must be
|
||||||
|
* one of "off", "on", or "disable_only"
|
||||||
|
*
|
||||||
|
* * `data-keydown-enabled` - whether keydown events should be listened to
|
||||||
|
*
|
||||||
|
* * `data-keyup-enabled` - whether keyup events should be listened to
|
||||||
*
|
*
|
||||||
* * `data-target` - the target to send live events to
|
* * `data-target` - the target to send live events to
|
||||||
*/
|
*/
|
||||||
|
|
@ -42,6 +48,8 @@ const KeyboardControl = {
|
||||||
|
|
||||||
getProps() {
|
getProps() {
|
||||||
return {
|
return {
|
||||||
|
cellId: getAttributeOrThrow(this.el, "data-cell-id"),
|
||||||
|
defaultHandlers: getAttributeOrThrow(this.el, "data-default-handlers"),
|
||||||
isKeydownEnabled: getAttributeOrThrow(
|
isKeydownEnabled: getAttributeOrThrow(
|
||||||
this.el,
|
this.el,
|
||||||
"data-keydown-enabled",
|
"data-keydown-enabled",
|
||||||
|
|
@ -57,22 +65,34 @@ const KeyboardControl = {
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDocumentKeyDown(event) {
|
handleDocumentKeyDown(event) {
|
||||||
|
if (
|
||||||
|
this.isKeyboardToggle(event) &&
|
||||||
|
!isEditableElement(document.activeElement)
|
||||||
|
) {
|
||||||
|
cancelEvent(event);
|
||||||
|
this.keyboardEnabled() ? this.disableKeyboard() : this.enableKeyboard();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (this.keyboardEnabled()) {
|
if (this.keyboardEnabled()) {
|
||||||
|
if (this.props.defaultHandlers !== "on") {
|
||||||
cancelEvent(event);
|
cancelEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.props.isKeydownEnabled) {
|
|
||||||
if (event.repeat) {
|
if (event.repeat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.props.isKeydownEnabled) {
|
||||||
const { key } = event;
|
const { key } = event;
|
||||||
this.pushEventTo(this.props.target, "keydown", { key });
|
this.pushEventTo(this.props.target, "keydown", { key });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDocumentKeyUp(event) {
|
handleDocumentKeyUp(event) {
|
||||||
if (this.keyboardEnabled()) {
|
if (this.keyboardEnabled()) {
|
||||||
|
if (this.props.defaultHandlers !== "on") {
|
||||||
cancelEvent(event);
|
cancelEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,10 +100,23 @@ const KeyboardControl = {
|
||||||
const { key } = event;
|
const { key } = event;
|
||||||
this.pushEventTo(this.props.target, "keyup", { key });
|
this.pushEventTo(this.props.target, "keyup", { key });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
handleDocumentFocus(event) {
|
handleDocumentFocus(event) {
|
||||||
if (this.props.isKeydownEnabled && isEditableElement(event.target)) {
|
if (this.props.isKeydownEnabled && isEditableElement(event.target)) {
|
||||||
|
this.disableKeyboard();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
enableKeyboard() {
|
||||||
|
if (!this.keyboardEnabled()) {
|
||||||
|
this.pushEventTo(this.props.target, "enable_keyboard", {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
disableKeyboard() {
|
||||||
|
if (this.keyboardEnabled()) {
|
||||||
this.pushEventTo(this.props.target, "disable_keyboard", {});
|
this.pushEventTo(this.props.target, "disable_keyboard", {});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
@ -91,6 +124,32 @@ const KeyboardControl = {
|
||||||
keyboardEnabled() {
|
keyboardEnabled() {
|
||||||
return this.props.isKeydownEnabled || this.props.isKeyupEnabled;
|
return this.props.isKeydownEnabled || this.props.isKeyupEnabled;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
isKeyboardToggle(event) {
|
||||||
|
if (event.repeat) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { metaKey, ctrlKey, key } = event;
|
||||||
|
const cmd = isMacOS() ? metaKey : ctrlKey;
|
||||||
|
|
||||||
|
if (cmd && key === "k" && this.isCellFocused()) {
|
||||||
|
return (
|
||||||
|
!this.keyboardEnabled() ||
|
||||||
|
["on", "disable_only"].includes(this.props.defaultHandlers)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
isCellFocused() {
|
||||||
|
const sessionEl = this.el.closest("[data-el-session]");
|
||||||
|
return (
|
||||||
|
sessionEl &&
|
||||||
|
sessionEl.getAttribute("data-js-focused-id") === this.props.cellId
|
||||||
|
);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default KeyboardControl;
|
export default KeyboardControl;
|
||||||
|
|
|
||||||
|
|
@ -1033,6 +1033,12 @@ const Session = {
|
||||||
setFocusedEl(focusableId, { scroll = true, focusElement = true } = {}) {
|
setFocusedEl(focusableId, { scroll = true, focusElement = true } = {}) {
|
||||||
this.focusedId = focusableId;
|
this.focusedId = focusableId;
|
||||||
|
|
||||||
|
if (focusableId) {
|
||||||
|
this.el.setAttribute("data-js-focused-id", focusableId);
|
||||||
|
} else {
|
||||||
|
this.el.removeAttribute("data-js-focused-id");
|
||||||
|
}
|
||||||
|
|
||||||
if (focusableId) {
|
if (focusableId) {
|
||||||
// If the element is inside collapsed section, expand that section
|
// If the element is inside collapsed section, expand that section
|
||||||
if (!this.isSection(focusableId)) {
|
if (!this.isSection(focusableId)) {
|
||||||
|
|
|
||||||
|
|
@ -253,14 +253,16 @@ defmodule LivebookWeb.Output do
|
||||||
id: id,
|
id: id,
|
||||||
input_views: input_views,
|
input_views: input_views,
|
||||||
session_pid: session_pid,
|
session_pid: session_pid,
|
||||||
client_id: client_id
|
client_id: client_id,
|
||||||
|
cell_id: cell_id
|
||||||
}) do
|
}) do
|
||||||
live_component(Output.ControlComponent,
|
live_component(Output.ControlComponent,
|
||||||
id: id,
|
id: id,
|
||||||
attrs: attrs,
|
attrs: attrs,
|
||||||
input_views: input_views,
|
input_views: input_views,
|
||||||
session_pid: session_pid,
|
session_pid: session_pid,
|
||||||
client_id: client_id
|
client_id: client_id,
|
||||||
|
cell_id: cell_id
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@ defmodule LivebookWeb.Output.ControlComponent do
|
||||||
class="flex"
|
class="flex"
|
||||||
id={"#{@id}-root"}
|
id={"#{@id}-root"}
|
||||||
phx-hook="KeyboardControl"
|
phx-hook="KeyboardControl"
|
||||||
|
data-cell-id={@cell_id}
|
||||||
|
data-default-handlers={Map.get(@attrs, :default_handlers, :off)}
|
||||||
data-keydown-enabled={to_string(@keyboard_enabled and :keydown in @attrs.events)}
|
data-keydown-enabled={to_string(@keyboard_enabled and :keydown in @attrs.events)}
|
||||||
data-keyup-enabled={to_string(@keyboard_enabled and :keyup in @attrs.events)}
|
data-keyup-enabled={to_string(@keyboard_enabled and :keyup in @attrs.events)}
|
||||||
data-target={@myself}
|
data-target={@myself}
|
||||||
|
|
@ -72,22 +74,19 @@ defmodule LivebookWeb.Output.ControlComponent do
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def handle_event("toggle_keyboard", %{}, socket) do
|
def handle_event("toggle_keyboard", %{}, socket) do
|
||||||
socket = update(socket, :keyboard_enabled, ¬/1)
|
enabled = !socket.assigns.keyboard_enabled
|
||||||
maybe_report_status(socket)
|
maybe_report_status(socket, enabled)
|
||||||
{:noreply, socket}
|
{:noreply, assign(socket, keyboard_enabled: enabled)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def handle_event("enable_keyboard", %{}, socket) do
|
||||||
|
maybe_report_status(socket, true)
|
||||||
|
{:noreply, assign(socket, keyboard_enabled: true)}
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("disable_keyboard", %{}, socket) do
|
def handle_event("disable_keyboard", %{}, socket) do
|
||||||
socket =
|
maybe_report_status(socket, false)
|
||||||
if socket.assigns.keyboard_enabled do
|
{:noreply, assign(socket, keyboard_enabled: false)}
|
||||||
socket = assign(socket, keyboard_enabled: false)
|
|
||||||
maybe_report_status(socket)
|
|
||||||
socket
|
|
||||||
else
|
|
||||||
socket
|
|
||||||
end
|
|
||||||
|
|
||||||
{:noreply, socket}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def handle_event("button_click", %{}, socket) do
|
def handle_event("button_click", %{}, socket) do
|
||||||
|
|
@ -105,9 +104,11 @@ defmodule LivebookWeb.Output.ControlComponent do
|
||||||
{:noreply, socket}
|
{:noreply, socket}
|
||||||
end
|
end
|
||||||
|
|
||||||
defp maybe_report_status(socket) do
|
defp maybe_report_status(socket, enabled) do
|
||||||
if :status in socket.assigns.attrs.events do
|
%{assigns: %{attrs: attrs, keyboard_enabled: current}} = socket
|
||||||
report_event(socket, %{type: :status, enabled: socket.assigns.keyboard_enabled})
|
|
||||||
|
if :status in attrs.events and enabled != current do
|
||||||
|
report_event(socket, %{type: :status, enabled: enabled})
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,13 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
|
||||||
%{seq: ["s", "r"], desc: "Show runtime panel"},
|
%{seq: ["s", "r"], desc: "Show runtime panel"},
|
||||||
%{seq: ["s", "b"], desc: "Show bin"},
|
%{seq: ["s", "b"], desc: "Show bin"},
|
||||||
%{seq: ["s", "p"], desc: "Show package search"},
|
%{seq: ["s", "p"], desc: "Show package search"},
|
||||||
%{seq: ["0", "0"], desc: "Reconnect current runtime"}
|
%{seq: ["0", "0"], desc: "Reconnect current runtime"},
|
||||||
|
%{
|
||||||
|
seq: ["ctrl", "k"],
|
||||||
|
seq_mac: ["⌘", "k"],
|
||||||
|
press_all: true,
|
||||||
|
desc: "Toggle keyboard control in cell output"
|
||||||
|
}
|
||||||
],
|
],
|
||||||
universal: [
|
universal: [
|
||||||
%{
|
%{
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue