Introduce code zen (#1115)

* Introduce code focus mode

* Update aria labels

* Update wording

* Show shortcut in code zen toggle

* Update shortcut
This commit is contained in:
Jonatan Kłosko 2022-04-14 21:17:19 +02:00 committed by GitHub
parent d3ebf42b32
commit 2b8d732a23
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 160 additions and 18 deletions

View file

@ -213,16 +213,24 @@
}
.menu__content {
@apply absolute z-[100] rounded-lg bg-white flex flex-col py-2 mt-1;
@apply absolute z-[100] rounded-lg bg-white flex flex-col py-2;
box-shadow: 0px 15px 99px rgba(13, 24, 41, 0.15);
}
.menu__content--right {
@apply right-0;
.menu__content--top-right {
@apply top-0 right-0 transform -translate-y-full -mt-1;
}
.menu__content--left {
@apply left-0;
.menu__content--top-left {
@apply top-0 left-0 transform -translate-y-full -mt-1;
}
.menu__content--bottom-right {
@apply bottom-0 right-0 transform translate-y-full -mb-1;
}
.menu__content--bottom-left {
@apply bottom-0 left-0 transform translate-y-full -mb-1;
}
.menu:not(.menu--open) > .menu__overlay,

View file

@ -259,3 +259,28 @@ solely client-side operations.
[phx-hook="VirtualizedLines"]:not(:hover) [data-el-clipcopy] {
@apply hidden;
}
[data-el-session][data-js-code-zen] [data-el-section-headline],
[data-el-session][data-js-code-zen] [data-el-section-subheadline],
[data-el-session][data-js-code-zen] [data-el-cell][data-type="markdown"],
[data-el-session][data-js-code-zen] [data-el-actions],
[data-el-session][data-js-code-zen] [data-el-insert-buttons] {
@apply hidden;
}
[data-el-session][data-js-code-zen] [data-el-sections-container] {
@apply space-y-0 mt-0;
}
[data-el-session][data-js-code-zen][data-js-no-outputs]
[data-el-outputs-container] {
@apply hidden;
}
[data-el-session][data-js-code-zen] [data-el-code-zen-enable] {
@apply hidden;
}
[data-el-session]:not([data-js-code-zen]) [data-el-focus-mode-options] {
@apply hidden;
}

View file

@ -1,5 +1,5 @@
import { getAttributeOrThrow, parseInteger } from "../lib/attribute";
import { randomId, randomToken } from "../lib/utils";
import { isElementHidden, randomId, randomToken } from "../lib/utils";
import {
getChannel,
transportDecode,
@ -227,7 +227,7 @@ const JSView = {
const { iframe, iframePlaceholder } = this;
const notebookEl = document.querySelector(`[data-el-notebook]`);
if (iframePlaceholder.offsetParent === null) {
if (isElementHidden(iframePlaceholder)) {
// When the placeholder is hidden, we hide the iframe as well
iframe.classList.add("hidden");
} else {

View file

@ -7,6 +7,7 @@ import {
setFavicon,
cancelEvent,
isElementInViewport,
isElementHidden,
} from "../lib/utils";
import { getAttributeOrDefault } from "../lib/attribute";
import KeyBuffer from "../lib/key_buffer";
@ -68,6 +69,7 @@ const Session = {
this.focusedId = null;
this.insertMode = false;
this.codeZen = false;
this.keyBuffer = new KeyBuffer();
this.clientsMap = {};
this.lastLocationReportByClientPid = {};
@ -121,6 +123,21 @@ const Session = {
this.handleCellIndicatorsClick(event)
);
this.getElement("code-zen-enable-button").addEventListener(
"click",
(event) => this.setCodeZen(true)
);
this.getElement("code-zen-disable-button").addEventListener(
"click",
(event) => this.setCodeZen(false)
);
this.getElement("code-zen-outputs-toggle").addEventListener(
"click",
(event) => this.el.toggleAttribute("data-js-no-outputs")
);
window.addEventListener(
"phx:page-loading-stop",
() => {
@ -359,9 +376,11 @@ const Session = {
} else if (keyBuffer.tryMatch(["N"])) {
this.insertCellAboveFocused("code");
} else if (keyBuffer.tryMatch(["m"])) {
this.insertCellBelowFocused("markdown");
!this.codeZen && this.insertCellBelowFocused("markdown");
} else if (keyBuffer.tryMatch(["M"])) {
this.insertCellAboveFocused("markdown");
!this.codeZen && this.insertCellAboveFocused("markdown");
} else if (keyBuffer.tryMatch(["z"])) {
this.setCodeZen(!this.codeZen);
}
}
},
@ -852,6 +871,28 @@ const Session = {
});
},
setCodeZen(enabled) {
this.codeZen = enabled;
if (enabled) {
this.el.setAttribute("data-js-code-zen", "");
} else {
this.el.removeAttribute("data-js-code-zen");
}
if (this.focusedId) {
const visibleId = this.ensureVisibleFocusableEl(this.focusedId);
if (visibleId !== this.focused) {
this.setFocusedEl(visibleId, { scroll: false });
}
if (visibleId) {
this.getFocusableEl(visibleId).scrollIntoView({ block: "center" });
}
}
},
// Server event handlers
handleCellInserted(cellId) {
@ -863,7 +904,12 @@ const Session = {
handleCellDeleted(cellId, siblingCellId) {
if (this.focusedId === cellId) {
this.setFocusedEl(siblingCellId);
if (this.codeZen) {
const visibleSiblingId = this.ensureVisibleFocusableEl(siblingCellId);
this.setFocusedEl(visibleSiblingId);
} else {
this.setFocusedEl(siblingCellId);
}
}
},
@ -1061,6 +1107,20 @@ const Session = {
}
},
ensureVisibleFocusableEl(cellId) {
const focusableEl = this.getFocusableEl(cellId);
const allFocusableEls = Array.from(
this.el.querySelectorAll(`[data-focusable-id]`)
);
const idx = allFocusableEls.indexOf(focusableEl);
const visibleSibling = [
...allFocusableEls.slice(idx, -1),
...allFocusableEls.slice(0, idx).reverse(),
].find((el) => !isElementHidden(el));
return visibleSibling && visibleSibling.getAttribute("data-focusable-id");
},
isCell(focusableId) {
const el = this.getFocusableEl(focusableId);
return el.hasAttribute("data-el-cell");
@ -1081,7 +1141,9 @@ const Session = {
},
getFocusableEls() {
return Array.from(this.el.querySelectorAll(`[data-focusable-id]`));
return Array.from(this.el.querySelectorAll(`[data-focusable-id]`)).filter(
(el) => !isElementHidden(el)
);
},
getFocusableIds() {

View file

@ -17,6 +17,10 @@ export function isElementInViewport(element) {
return box.bottom >= 0 && box.top <= window.innerHeight;
}
export function isElementHidden(element) {
return element.offsetParent === null;
}
export function clamp(n, x, y) {
return Math.min(Math.max(n, x), y);
}

View file

@ -193,7 +193,7 @@ defmodule LivebookWeb.FileSelectComponent do
defp file_system_menu_button(assigns) do
~H"""
<.menu id="file-system-menu" disabled={@file_system_select_disabled} position="left">
<.menu id="file-system-menu" disabled={@file_system_select_disabled} position="bottom-left">
<:toggle>
<button type="button" class="button-base button-gray button-square-icon"
aria-label="switch file system"

View file

@ -438,7 +438,7 @@ defmodule LivebookWeb.LiveHelpers do
assigns =
assigns
|> assign_new(:disabled, fn -> false end)
|> assign_new(:position, fn -> "right" end)
|> assign_new(:position, fn -> "bottom-right" end)
|> assign_new(:secondary_click, fn -> false end)
~H"""
@ -452,7 +452,7 @@ defmodule LivebookWeb.LiveHelpers do
</div>
<div class="menu__overlay"
phx-click-away={JS.remove_class("menu--open", to: "##{@id}")}></div>
<menu class={"menu__content menu__content--#{@position}"}
<menu class={"menu__content #{menu_content_class(@position)}"}
role="menu"
phx-click-away={JS.remove_class("menu--open", to: "##{@id}")}}>
<%= render_slot(@content) %>
@ -461,6 +461,11 @@ defmodule LivebookWeb.LiveHelpers do
"""
end
defp menu_content_class("top-left"), do: "menu__content--top-left"
defp menu_content_class("top-right"), do: "menu__content--top-right"
defp menu_content_class("bottom-left"), do: "menu__content--bottom-left"
defp menu_content_class("bottom-right"), do: "menu__content--bottom-right"
@doc """
A menu item that shows a submenu on hover.

View file

@ -198,7 +198,7 @@ defmodule LivebookWeb.SessionLive do
runtime={@data_view.runtime}
cell_view={@data_view.setup_cell_view} />
</div>
<div class="mt-8 flex flex-col w-full space-y-16">
<div class="mt-8 flex flex-col w-full space-y-16" data-el-sections-container>
<%= if @data_view.section_views == [] do %>
<div class="flex justify-center">
<button class="button-base button-small"

View file

@ -5,6 +5,7 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
def render(assigns) do
~H"""
<div class="flex flex-col items-center space-y-2" data-el-notebook-indicators>
<.code_zen_indicator />
<%= if @file do %>
<%= if @dirty do %>
<%= if @autosave_interval_s do %>
@ -67,6 +68,42 @@ defmodule LivebookWeb.SessionLive.IndicatorsComponent do
"""
end
defp code_zen_indicator(assigns) do
~H"""
<span class="tooltip left" data-tooltip="Enter code zen (z)" data-el-code-zen-enable>
<button class="icon-button icon-outlined-button border-gray-200 hover:bg-gray-100 focus:bg-gray-100"
aria-label="enter code zen"
data-el-code-zen-enable-button>
<.remix_icon icon="code-line" class="text-xl text-gray-400" />
</button>
</span>
<div data-el-focus-mode-options>
<.menu id="focus-mode-menu" position="top-right">
<:toggle>
<button class="icon-button icon-outlined-button border-green-bright-300 hover:bg-green-bright-50 focus:bg-green-bright-50"
aria-label="code zen options">
<.remix_icon icon="code-line" class="text-xl text-green-bright-400" />
</button>
</:toggle>
<:content>
<button class="menu-item text-gray-500"
role="menuitem"
data-el-code-zen-outputs-toggle>
<.remix_icon icon="layout-bottom-2-line" />
<span class="font-medium">Toggle outputs</span>
</button>
<button class="menu-item text-gray-500"
role="menuitem"
data-el-code-zen-disable-button>
<.remix_icon icon="close-line" />
<span class="font-medium">Exit code zen</span>
</button>
</:content>
</.menu>
</div>
"""
end
defp global_status(%{status: :evaluating} = assigns) do
~H"""
<span class="tooltip left" data-tooltip="Go to evaluating cell">

View file

@ -14,7 +14,7 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
phx-value-section_id={@section_id}
phx-value-cell_id={@cell_id}
>+ Code</button>
<.menu id={"#{@id}-block-menu"} position="left">
<.menu id={"#{@id}-block-menu"} position="bottom-left">
<:toggle>
<button class="button-base button-small">+ Block</button>
</:toggle>
@ -61,7 +61,7 @@ defmodule LivebookWeb.SessionLive.InsertButtonsComponent do
</span>
<% true -> %>
<.menu id={"#{@id}-smart-menu"} position="left">
<.menu id={"#{@id}-smart-menu"} position="bottom-left">
<:toggle>
<button class="button-base button-small">+ Smart</button>
</:toggle>

View file

@ -89,7 +89,7 @@ defmodule LivebookWeb.SessionLive.SectionComponent do
</div>
</div>
<%= if @section_view.parent do %>
<h3 class="mt-1 flex items-end space-x-1 text-sm font-semibold text-gray-800">
<h3 class="mt-1 flex items-end space-x-1 text-sm font-semibold text-gray-800" data-el-section-subheadline>
<span class="tooltip bottom" data-tooltip={"This section branches out from the main flow\nand can be evaluated in parallel"}>
<.remix_icon icon="git-branch-line" class="text-lg font-normal flip-horizontally leading-none" />
</span>

View file

@ -91,6 +91,7 @@ defmodule LivebookWeb.SessionLive.ShortcutsComponent do
%{seq: ["m"], desc: "Insert Markdown cell below", basic: true},
%{seq: ["N"], desc: "Insert Code cell above"},
%{seq: ["M"], desc: "Insert Markdown cell above"},
%{seq: ["z"], desc: "Toggle code zen"},
%{seq: ["d", "d"], desc: "Delete cell", basic: true},
%{seq: ["e", "e"], desc: "Evaluate cell"},
%{seq: ["e", "s"], desc: "Evaluate section"},