From ad4867ddfb6e4ec521be2814f3cc1bcc75546843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jonatan=20K=C5=82osko?= Date: Tue, 2 Nov 2021 22:34:44 +0100 Subject: [PATCH] Refactor modals with JS commands (#669) * Use JS commands for closing the modal with animations * Refactor modal to render content as slot * Bump LV --- assets/css/utilities.css | 28 ++++++ assets/js/session/index.js | 4 +- assets/package-lock.json | 6 +- lib/livebook_web.ex | 1 + lib/livebook_web/helpers.ex | 74 ++++++++------ lib/livebook_web/live/explore_live.ex | 9 +- lib/livebook_web/live/home_live.ex | 32 +++--- lib/livebook_web/live/modal_component.ex | 47 --------- lib/livebook_web/live/session_live.ex | 118 ++++++++++++----------- lib/livebook_web/live/settings_live.ex | 30 +++--- lib/livebook_web/live/user_helpers.ex | 18 ++++ mix.lock | 2 +- 12 files changed, 194 insertions(+), 175 deletions(-) delete mode 100644 lib/livebook_web/live/modal_component.ex diff --git a/assets/css/utilities.css b/assets/css/utilities.css index fde759cff..a4e4b6a5e 100644 --- a/assets/css/utilities.css +++ b/assets/css/utilities.css @@ -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; + } + } } diff --git a/assets/js/session/index.js b/assets/js/session/index.js index 640ec3a55..74c7f4172 100644 --- a/assets/js/session/index.js +++ b/assets/js/session/index.js @@ -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; } diff --git a/assets/package-lock.json b/assets/package-lock.json index bfeb193d3..f3e4d8e3c 100644 --- a/assets/package-lock.json +++ b/assets/package-lock.json @@ -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": { diff --git a/lib/livebook_web.ex b/lib/livebook_web.ex index b7b04ed48..c12aa36cf 100644 --- a/lib/livebook_web.ex +++ b/lib/livebook_web.ex @@ -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 diff --git a/lib/livebook_web/helpers.ex b/lib/livebook_web/helpers.ex index 1eea03dd5..9eb813544 100644 --- a/lib/livebook_web/helpers.ex +++ b/lib/livebook_web/helpers.ex @@ -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} /> + """ - 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""" +
+ +
+ + + + + +
+
+ """ 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 """ diff --git a/lib/livebook_web/live/explore_live.ex b/lib/livebook_web/live/explore_live.ex index b8aac9174..658f4fd95 100644 --- a/lib/livebook_web/live/explore_live.ex +++ b/lib/livebook_web/live/explore_live.ex @@ -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 <%= 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 diff --git a/lib/livebook_web/live/home_live.ex b/lib/livebook_web/live/home_live.ex index c589083d6..c357cfc53 100644 --- a/lib/livebook_web/live/home_live.ex +++ b/lib/livebook_web/live/home_live.ex @@ -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 <%= 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} /> + <% 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} /> + <% end %> """ end diff --git a/lib/livebook_web/live/modal_component.ex b/lib/livebook_web/live/modal_component.ex deleted file mode 100644 index a179d775e..000000000 --- a/lib/livebook_web/live/modal_component.ex +++ /dev/null @@ -1,47 +0,0 @@ -defmodule LivebookWeb.ModalComponent do - use LivebookWeb, :live_component - - @impl true - def render(assigns) do - ~H""" -
- - -
- - - - - -
-
- """ - end - - @impl true - def handle_event("close", _params, socket) do - {:noreply, push_patch(socket, to: socket.assigns.return_to)} - end -end diff --git a/lib/livebook_web/live/session_live.ex b/lib/livebook_web/live/session_live.ex index 080a07d8a..519f291e2 100644 --- a/lib/livebook_web/live/session_live.ex +++ b/lib/livebook_web/live/session_live.ex @@ -267,88 +267,90 @@ defmodule LivebookWeb.SessionLive do <%= 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} /> + <% 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 + } %> + <% 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} /> + <% 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} /> + <% 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} /> + <% 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} /> + <% 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} /> + <% 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} /> + <% end %> """ end diff --git a/lib/livebook_web/live/settings_live.ex b/lib/livebook_web/live/settings_live.ex index a0cf364d6..7985478cb 100644 --- a/lib/livebook_web/live/settings_live.ex +++ b/lib/livebook_web/live/settings_live.ex @@ -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 <%= 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)} /> + <% 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} /> + <% end %> """ end diff --git a/lib/livebook_web/live/user_helpers.ex b/lib/livebook_web/live/user_helpers.ex index 20742f979..f42824a07 100644 --- a/lib/livebook_web/live/user_helpers.ex +++ b/lib/livebook_web/live/user_helpers.ex @@ -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""" + + <.live_component module={LivebookWeb.UserComponent} + id="user" + return_to={@return_to} + user={@current_user} /> + + """ + end end diff --git a/mix.lock b/mix.lock index 94626859f..d56f97c8b 100644 --- a/mix.lock +++ b/mix.lock @@ -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"},