Refactor modals with JS commands (#669)

* Use JS commands for closing the modal with animations

* Refactor modal to render content as slot

* Bump LV
This commit is contained in:
Jonatan Kłosko 2021-11-02 22:34:44 +01:00 committed by GitHub
parent 3afa81f454
commit ad4867ddfb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 194 additions and 175 deletions

View file

@ -30,4 +30,32 @@
.scroll-smooth {
scroll-behavior: smooth;
}
/* Animations */
.fade-in {
animation: fade-in-frames 200ms;
}
.fade-out {
animation: fade-out-frames 200ms;
}
@keyframes fade-in-frames {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fade-out-frames {
from {
opacity: 1;
}
to {
opacity: 0;
}
}
}

View file

@ -393,8 +393,8 @@ function handleDocumentMouseDown(hook, event) {
return;
}
// If primary cell action is clicked, keep the focus as is
if (event.target.closest(`[data-element="actions"][data-primary]`)) {
// If a cell action is clicked, keep the focus as is
if (event.target.closest(`[data-element="actions"]`)) {
return;
}

View file

@ -54,14 +54,14 @@
}
},
"../deps/phoenix": {
"version": "1.5.12",
"version": "1.6.2",
"license": "MIT"
},
"../deps/phoenix_html": {
"version": "3.0.0"
"version": "3.1.0"
},
"../deps/phoenix_live_view": {
"version": "0.16.0",
"version": "0.17.4",
"license": "MIT"
},
"node_modules/@babel/code-frame": {

View file

@ -62,6 +62,7 @@ defmodule LivebookWeb do
# Import basic rendering functionality (render, render_layout, etc)
import Phoenix.View
alias Phoenix.LiveView.JS
alias LivebookWeb.Router.Helpers, as: Routes
# Custom helpers

View file

@ -1,44 +1,60 @@
defmodule LivebookWeb.Helpers do
use Phoenix.Component
alias Phoenix.LiveView.JS
alias LivebookWeb.Router.Helpers, as: Routes
alias Livebook.FileSystem
@doc """
Renders a component inside the `Livebook.ModalComponent` component.
Wraps the given content in a modal dialog.
The rendered modal receives a `:return_to` option to properly update
the URL when the modal is closed.
When closed, the modal redirects to the given `:return_to` URL.
## Example
<.live_modal return_to={...}>
<.live_component module={MyComponent} />
</.live_modal>
"""
def live_modal(component, opts) do
{modal_opts, opts} = build_modal_opts(opts)
modal_opts = [{:render_spec, {:component, component, opts}} | modal_opts]
live_component(LivebookWeb.ModalComponent, modal_opts)
def modal(assigns) do
assigns =
assigns
|> assign_new(:class, fn -> "" end)
~H"""
<div class="fixed z-[10000] inset-0 fade-in" phx-remove={JS.transition("fade-out")}>
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
<!-- Overlay -->
<div class="absolute inset-0 bg-gray-500 opacity-75 z-0" aria-hidden="true"></div>
<!-- Modal box -->
<div class={"relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl #{@class}"}
role="dialog"
aria-modal="true"
phx-window-keydown={click_modal_close()}
phx-click-away={click_modal_close()}
phx-key="escape">
<%= live_patch to: @return_to,
class: "absolute top-6 right-6 text-gray-400 flex space-x-1 items-center",
aria_label: "close modal",
id: "close-modal-button" do %>
<span class="text-sm">(esc)</span>
<.remix_icon icon="close-line" class="text-2xl" />
<% end %>
<%= render_slot(@inner_block) %>
</div>
</div>
</div>
"""
end
@doc """
Renders a live view inside the `Livebook.ModalComponent` component.
See `live_modal/2` for more details.
"""
def live_modal(socket, live_view, opts) do
{modal_opts, opts} = build_modal_opts(opts)
modal_opts = [{:render_spec, {:live_view, socket, live_view, opts}} | modal_opts]
live_component(LivebookWeb.ModalComponent, modal_opts)
end
defp build_modal_opts(opts) do
path = Keyword.fetch!(opts, :return_to)
{modal_class, opts} = Keyword.pop(opts, :modal_class)
modal_opts = [
id: "modal",
return_to: path,
modal_class: modal_class
]
{modal_opts, opts}
defp click_modal_close(js \\ %JS{}) do
JS.dispatch(js, "click", to: "#close-modal-button")
end
@doc """

View file

@ -2,6 +2,7 @@ defmodule LivebookWeb.ExploreLive do
use LivebookWeb, :live_view
import LivebookWeb.SessionHelpers
import LivebookWeb.UserHelpers
alias LivebookWeb.{SidebarHelpers, ExploreHelpers, PageHelpers}
alias Livebook.Notebook.Explore
@ -70,11 +71,9 @@ defmodule LivebookWeb.ExploreLive do
</div>
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: "user",
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.explore_path(@socket, :page) %>
<.current_user_modal
return_to={Routes.explore_path(@socket, :page)}
current_user={@current_user} />
<% end %>
"""
end

View file

@ -2,6 +2,7 @@ defmodule LivebookWeb.HomeLive do
use LivebookWeb, :live_view
import LivebookWeb.SessionHelpers
import LivebookWeb.UserHelpers
alias LivebookWeb.{SidebarHelpers, ExploreHelpers}
alias Livebook.{Sessions, Session, LiveMarkdown, Notebook, FileSystem}
@ -115,28 +116,27 @@ defmodule LivebookWeb.HomeLive do
</div>
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: "user",
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.home_path(@socket, :page) %>
<.current_user_modal
return_to={Routes.home_path(@socket, :page)}
current_user={@current_user} />
<% end %>
<%= if @live_action == :close_session do %>
<%= live_modal LivebookWeb.HomeLive.CloseSessionComponent,
id: "close-session",
modal_class: "w-full max-w-xl",
return_to: Routes.home_path(@socket, :page),
session: @session %>
<.modal class="w-full max-w-xl" return_to={Routes.home_path(@socket, :page)}>
<.live_component module={LivebookWeb.HomeLive.CloseSessionComponent}
id="close-session"
return_to={Routes.home_path(@socket, :page)}
session={@session} />
</.modal>
<% end %>
<%= if @live_action == :import do %>
<%= live_modal LivebookWeb.HomeLive.ImportComponent,
id: "import",
modal_class: "w-full max-w-xl",
return_to: Routes.home_path(@socket, :page),
tab: @tab,
import_opts: @import_opts %>
<.modal class="w-full max-w-xl" return_to={Routes.home_path(@socket, :page)}>
<.live_component module={LivebookWeb.HomeLive.ImportComponent}
id="import"
tab={@tab}
import_opts={@import_opts} />
</.modal>
<% end %>
"""
end

View file

@ -1,47 +0,0 @@
defmodule LivebookWeb.ModalComponent do
use LivebookWeb, :live_component
@impl true
def render(assigns) do
~H"""
<div class="fixed z-[10000] inset-0">
<!-- Modal container -->
<div class="h-screen flex items-center justify-center p-4">
<!-- Overlay -->
<div class="absolute inset-0 bg-gray-500 opacity-75 z-0"
aria-hidden="true"
phx-window-keydown="close"
phx-key="escape"
phx-target={@myself}
phx-page-loading></div>
<!-- Modal box -->
<div class={"relative max-h-full overflow-y-auto bg-white rounded-lg shadow-xl #{@modal_class}"}
phx-click-away="close"
phx-target={@myself}
role="dialog"
aria-modal="true">
<%= live_patch to: @return_to, class: "absolute top-6 right-6 text-gray-400 flex space-x-1 items-center" do %>
<span class="text-sm">(esc)</span>
<.remix_icon icon="close-line" class="text-2xl" />
<% end %>
<%=
case @render_spec do
{:component, component, opts} -> live_component(component, opts)
{:live_view, socket, live_view, opts} -> live_render(socket, live_view, opts)
end
%>
</div>
</div>
</div>
"""
end
@impl true
def handle_event("close", _params, socket) do
{:noreply, push_patch(socket, to: socket.assigns.return_to)}
end
end

View file

@ -267,88 +267,90 @@ defmodule LivebookWeb.SessionLive do
</div>
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: "user",
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.session_path(@socket, :page, @session.id) %>
<.current_user_modal
return_to={Routes.session_path(@socket, :page, @session.id)}
current_user={@current_user} />
<% end %>
<%= if @live_action == :runtime_settings do %>
<%= live_modal LivebookWeb.SessionLive.RuntimeComponent,
id: "runtime-settings",
modal_class: "w-full max-w-4xl",
return_to: Routes.session_path(@socket, :page, @session.id),
session: @session,
runtime: @data_view.runtime %>
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<.live_component module={LivebookWeb.SessionLive.RuntimeComponent}
id="runtime-settings"
session={@session}
runtime={@data_view.runtime} />
</.modal>
<% end %>
<%= if @live_action == :file_settings do %>
<%= live_modal @socket, LivebookWeb.SessionLive.PersistenceLive,
id: "persistence",
modal_class: "w-full max-w-4xl",
return_to: Routes.session_path(@socket, :page, @session.id),
session: %{
"session" => @session,
"file" => @data_view.file,
"persist_outputs" => @data_view.persist_outputs,
"autosave_interval_s" => @data_view.autosave_interval_s
} %>
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<%= live_render @socket, LivebookWeb.SessionLive.PersistenceLive,
id: "persistence",
session: %{
"session" => @session,
"file" => @data_view.file,
"persist_outputs" => @data_view.persist_outputs,
"autosave_interval_s" => @data_view.autosave_interval_s
} %>
</.modal>
<% end %>
<%= if @live_action == :shortcuts do %>
<%= live_modal LivebookWeb.SessionLive.ShortcutsComponent,
id: "shortcuts",
modal_class: "w-full max-w-6xl",
platform: @platform,
return_to: Routes.session_path(@socket, :page, @session.id) %>
<.modal class="w-full max-w-6xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<.live_component module={LivebookWeb.SessionLive.ShortcutsComponent}
id="shortcuts"
platform={@platform} />
</.modal>
<% end %>
<%= if @live_action == :cell_settings do %>
<%= live_modal settings_component_for(@cell),
id: "cell-settings",
modal_class: "w-full max-w-xl",
session: @session,
cell: @cell,
return_to: Routes.session_path(@socket, :page, @session.id) %>
<.modal class="w-full max-w-xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<.live_component module={settings_component_for(@cell)}
id="cell-settings"
session={@session}
return_to={Routes.session_path(@socket, :page, @session.id)}
cell={@cell} />
</.modal>
<% end %>
<%= if @live_action == :cell_upload do %>
<%= live_modal LivebookWeb.SessionLive.CellUploadComponent,
id: "cell-upload",
modal_class: "w-full max-w-xl",
session: @session,
cell: @cell,
uploads: @uploads,
return_to: Routes.session_path(@socket, :page, @session.id) %>
<.modal class="w-full max-w-xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<.live_component module={LivebookWeb.SessionLive.CellUploadComponent}
id="cell-upload"
session={@session}
return_to={Routes.session_path(@socket, :page, @session.id)}
cell={@cell}
uploads={@uploads} />
</.modal>
<% end %>
<%= if @live_action == :delete_section do %>
<%= live_modal LivebookWeb.SessionLive.DeleteSectionComponent,
id: "delete-section",
modal_class: "w-full max-w-xl",
session: @session,
section: @section,
is_first: @section.id == @first_section_id,
return_to: Routes.session_path(@socket, :page, @session.id) %>
<.modal class="w-full max-w-xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<.live_component module={LivebookWeb.SessionLive.DeleteSectionComponent}
id="delete-section"
session={@session}
return_to={Routes.session_path(@socket, :page, @session.id)}
section={@section}
is_first={@section.id == @first_section_id} />
</.modal>
<% end %>
<%= if @live_action == :bin do %>
<%= live_modal LivebookWeb.SessionLive.BinComponent,
id: "bin",
modal_class: "w-full max-w-4xl",
session: @session,
bin_entries: @data_view.bin_entries,
return_to: Routes.session_path(@socket, :page, @session.id) %>
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<.live_component module={LivebookWeb.SessionLive.BinComponent}
id="bin"
session={@session}
return_to={Routes.session_path(@socket, :page, @session.id)}
bin_entries={@data_view.bin_entries} />
</.modal>
<% end %>
<%= if @live_action == :export do %>
<%= live_modal LivebookWeb.SessionLive.ExportComponent,
id: "export",
modal_class: "w-full max-w-4xl",
session: @session,
tab: @tab,
return_to: Routes.session_path(@socket, :page, @session.id) %>
<.modal class="w-full max-w-4xl" return_to={Routes.session_path(@socket, :page, @session.id)}>
<.live_component module={LivebookWeb.SessionLive.ExportComponent}
id="export"
session={@session}
tab={@tab} />
</.modal>
<% end %>
"""
end

View file

@ -1,6 +1,8 @@
defmodule LivebookWeb.SettingsLive do
use LivebookWeb, :live_view
import LivebookWeb.UserHelpers
alias LivebookWeb.{SidebarHelpers, PageHelpers}
@impl true
@ -66,26 +68,26 @@ defmodule LivebookWeb.SettingsLive do
</div>
<%= if @live_action == :user do %>
<%= live_modal LivebookWeb.UserComponent,
id: "user",
modal_class: "w-full max-w-sm",
user: @current_user,
return_to: Routes.settings_path(@socket, :page) %>
<.current_user_modal
return_to={Routes.settings_path(@socket, :page)}
current_user={@current_user} />
<% end %>
<%= if @live_action == :add_file_system do %>
<%= live_modal LivebookWeb.SettingsLive.AddFileSystemComponent,
id: "add-file-system",
modal_class: "w-full max-w-3xl",
return_to: Routes.settings_path(@socket, :page) %>
<.modal class="w-full max-w-3xl" return_to={Routes.settings_path(@socket, :page)}>
<.live_component module={LivebookWeb.SettingsLive.AddFileSystemComponent}
id="add-file-system"
return_to={Routes.settings_path(@socket, :page)} />
</.modal>
<% end %>
<%= if @live_action == :detach_file_system do %>
<%= live_modal LivebookWeb.SettingsLive.RemoveFileSystemComponent,
id: "detach-file-system",
modal_class: "w-full max-w-xl",
file_system: @file_system,
return_to: Routes.settings_path(@socket, :page) %>
<.modal class="w-full max-w-xl" return_to={Routes.settings_path(@socket, :page)}>
<.live_component module={LivebookWeb.SettingsLive.RemoveFileSystemComponent}
id="detach-file-system"
return_to={Routes.settings_path(@socket, :page)}
file_system={@file_system} />
</.modal>
<% end %>
"""
end

View file

@ -37,4 +37,22 @@ defmodule LivebookWeb.UserHelpers do
initials -> List.first(initials) <> List.last(initials)
end
end
@doc """
Renders the current user edit form in a modal.
## Examples
<.current_user_modal return_to={...} current_user={@current_user} />
"""
def current_user_modal(assigns) do
~H"""
<LivebookWeb.Helpers.modal class="w-full max-w-sm" return_to={@return_to}>
<.live_component module={LivebookWeb.UserComponent}
id="user"
return_to={@return_to}
user={@current_user} />
</LivebookWeb.Helpers.modal>
"""
end
end

View file

@ -15,7 +15,7 @@
"phoenix_html": {:hex, :phoenix_html, "3.1.0", "0b499df05aad27160d697a9362f0e89fa0e24d3c7a9065c2bd9d38b4d1416c09", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "0c0a98a2cefa63433657983a2a594c7dee5927e4391e0f1bfd3a151d1def33fc"},
"phoenix_live_dashboard": {:hex, :phoenix_live_dashboard, "0.6.1", "fb94a33c077141f9ac7930b322a7a3b99f9b144bf3a08dd667b9f9aaf0319889", [:mix], [{:ecto, "~> 3.6.2 or ~> 3.7", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_mysql_extras, "~> 0.3", [hex: :ecto_mysql_extras, repo: "hexpm", optional: true]}, {:ecto_psql_extras, "~> 0.7", [hex: :ecto_psql_extras, repo: "hexpm", optional: true]}, {:mime, "~> 1.6.0", [hex: :mime, repo: "hexpm", optional: false]}, {:phoenix_live_view, "~> 0.17.1", [hex: :phoenix_live_view, repo: "hexpm", optional: false]}, {:telemetry_metrics, "~> 0.6.0", [hex: :telemetry_metrics, repo: "hexpm", optional: false]}], "hexpm", "6faf1373e5846c8ab68c2cf55cfa5c196c1fbbe0c72d12a4cdfaaac6ef189948"},
"phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.4", "c994c248c1195ccdb25a4e990eadabddb34c59d8f620d5c0aec418e2b7913651", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "86d27af55e78bd42476a4084d54c3add660e565c07233d8afacb20ab6d7f53b6"},
"phoenix_live_view": {:hex, :phoenix_live_view, "0.17.5", "63f52a6f9f6983f04e424586ff897c016ecc5e4f8d1e2c22c2887af1c57215d8", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.5.9 or ~> 1.6.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c5586e6a3d4df71b8214c769d4f5eb8ece2b4001711a7ca0f97323c36958b0e3"},
"phoenix_pubsub": {:hex, :phoenix_pubsub, "2.0.0", "a1ae76717bb168cdeb10ec9d92d1480fec99e3080f011402c0a2d68d47395ffb", [:mix], [], "hexpm", "c52d948c4f261577b9c6fa804be91884b381a7f8f18450c5045975435350f771"},
"phoenix_view": {:hex, :phoenix_view, "1.0.0", "fea71ecaaed71178b26dd65c401607de5ec22e2e9ef141389c721b3f3d4d8011", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "82be3e2516f5633220246e2e58181282c71640dab7afc04f70ad94253025db0c"},
"plug": {:hex, :plug, "1.12.1", "645678c800601d8d9f27ad1aebba1fdb9ce5b2623ddb961a074da0b96c35187d", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d57e799a777bc20494b784966dc5fbda91eb4a09f571f76545b72a634ce0d30b"},