Skip to content

Commit

Permalink
feat(component): add Modal component
Browse files Browse the repository at this point in the history
  • Loading branch information
Goose97 committed Mar 25, 2024
1 parent 201f784 commit f494f42
Show file tree
Hide file tree
Showing 5 changed files with 223 additions and 3 deletions.
Binary file added assets/modal-example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
4 changes: 2 additions & 2 deletions lib/component/input.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ defmodule Orange.Component.Input do
## Attributes
- `:on_submit` - A callback triggered when input is submitted. This attribute is required.
- `:submit_key` - The keyboard key that will trigger submission. This attribute is option and defaults to :enter. See `Orange.Terminal.KeyEvent` for supported values.
- `:submit_key` - The keyboard key that will trigger submission. This attribute is optional and defaults to :enter. See `Orange.Terminal.KeyEvent` for supported values.
- `:on_exit` - A callback triggered when input is exited. This attribute is optional.
- `:exit_key` - The keyboard key that will unfocus the input. This attribute is option and defaults to :esc. See `Orange.Terminal.KeyEvent` for supported values.
- `:exit_key` - The keyboard key that will unfocus the input. This attribute is optional and defaults to :esc. See `Orange.Terminal.KeyEvent` for supported values.
> #### Info {: .info}
>
> The `:submit_key` and `:exit_key` can not be `:backspace` as it is reserved for deleting characters.
Expand Down
76 changes: 76 additions & 0 deletions lib/component/modal.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
defmodule Orange.Component.Modal do
@moduledoc """
An modal/dialog component which renders an overlay in the middle of the viewport.
## Attributes
- `:open` - Whether the modal is open or not. This attribute is required.
- `:offset_x` - The offset from the left and right edge of the screen. This attribute is required.
- `:offset_y` - The offset from the top and bottom edge of the screen. This attribute is required.
> #### Info {: .info}
>
> When the offset_x/y is too big for the terminal size, the modal will not be rendered.
- `:children` - The content of the modal. This attribute is optional.
## Examples
defmodule Example do
@behaviour Orange.Component
import Orange.Macro
@impl true
def init(_attrs), do: %{state: %{search_value: ""}}
@impl true
def render(state, _attrs, update) do
modal_content =
rect do
"foo"
"bar"
end
rect do
line do
"Displaying modal..."
end
{
Orange.Component.Modal,
offset_x: 8,
offset_y: 4,
children: modal_content,
open: true
}
end
end
end
![rendered result](assets/modal-example.png)
"""

@behaviour Orange.Component

import Orange.Macro

@impl true
def init(_attrs), do: %{state: nil}

@impl true
def render(_state, attrs, _update) do
{width, height} = terminal_impl().terminal_size()

offset_x = attrs[:offset_x]
offset_y = attrs[:offset_y]

# Plus 2 for the border
if attrs[:open] && width > offset_x * 2 + 2 && height > offset_y * 2 + 2 do
rect position: {:fixed, offset_y, offset_x, offset_y, offset_x}, style: [border: true] do
attrs[:children]
end
end
end

defp terminal_impl(), do: Application.get_env(:orange, :terminal, Orange.Terminal)
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ defmodule Orange.MixProject do
Components: [
Orange.Component,
Orange.Component.VerticalScrollableRect,
Orange.Component.Input
Orange.Component.Input,
Orange.Component.Modal
],
Events: [
Orange.Terminal.KeyEvent
Expand Down
143 changes: 143 additions & 0 deletions test/component/modal_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
defmodule Orange.Component.ModalTest do
use ExUnit.Case
import Mox

alias Orange.Renderer.Buffer
alias Orange.{Terminal, RuntimeTestHelper}

setup_all do
Mox.defmock(Orange.MockTerminal, for: Terminal)
Application.put_env(:orange, :terminal, Orange.MockTerminal)

:ok
end

setup :set_mox_from_context
setup :verify_on_exit!

test ":open is true" do
RuntimeTestHelper.setup_mock_terminal(Orange.MockTerminal,
terminal_size: {20, 15}
)

buffer = RuntimeTestHelper.dry_render_once({__MODULE__.Modal, open: true})

assert Buffer.to_string(buffer) === """
Displaying modal...-
--------------------
--------------------
--------------------
----┌──────────┐----
----│foo-------│----
----│bar-------│----
----│----------│----
----│----------│----
----│----------│----
----└──────────┘----
--------------------
--------------------
--------------------
--------------------\
"""
end

test ":open is false" do
RuntimeTestHelper.setup_mock_terminal(Orange.MockTerminal,
terminal_size: {20, 15}
)

buffer = RuntimeTestHelper.dry_render_once({__MODULE__.Modal, open: false})

assert Buffer.to_string(buffer) === """
Displaying modal...-
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------\
"""
end

test "offset_x is too big for width" do
RuntimeTestHelper.setup_mock_terminal(Orange.MockTerminal,
terminal_size: {20, 6}
)

buffer =
RuntimeTestHelper.dry_render_once({__MODULE__.Modal, open: true, offset_x: 10, offset_y: 1})

assert Buffer.to_string(buffer) === """
Displaying modal...-
--------------------
--------------------
--------------------
--------------------
--------------------\
"""
end

test "offset_y is too big for height" do
RuntimeTestHelper.setup_mock_terminal(Orange.MockTerminal,
terminal_size: {20, 12}
)

buffer = RuntimeTestHelper.dry_render_once({__MODULE__.Modal, open: true, offset_y: 6})

assert Buffer.to_string(buffer) === """
Displaying modal...-
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------
--------------------\
"""
end

defmodule Modal do
@behaviour Orange.Component

import Orange.Macro
alias Orange.Component

@impl true
def init(_attrs), do: %{state: nil}

@impl true
def render(_state, attrs, _update) do
offset_x = Keyword.get(attrs, :offset_x, 4)
offset_y = Keyword.get(attrs, :offset_y, 4)

modal_content =
rect do
"foo"
"bar"
end

rect style: [width: "100%", height: "100%"] do
line do
"Displaying modal..."
end

{
Component.Modal,
offset_x: offset_x, offset_y: offset_y, children: modal_content, open: attrs[:open]
}
end
end
end
end

0 comments on commit f494f42

Please sign in to comment.