defmodule Shino.UI.Sheet do
@moduledoc """
Provides sheet related components.
> Displays content that complements the main content of the screen.
## Related components
* `Shino.UI.Dialog`
## Availale components
Components are divided into two categories: control components and style components.
Control components:
* ``
* ``
* ``
* ``
Style components:
* ``
* ``
* ``
* ``
## Opening and closing a sheet
For opening a sheet, you can use:
* `:default_open` attr of `` component
* `` component
For closing a sheet, you can use:
* `` component
* the builtin user interactions:
* clicking outside of the sheet
* pressing ESC key
## Controlling the side of a sheet
Use `:side` attr of of `` component.
## Controlling the width of a sheet
By default, the sheet width is determined by its content.
This behaviour can be customized by applying classes to ``
component. For example:
```heex
<% # content %>
```
## Examples
### A basic sheet
```heex
<.button>Open Basic Sheet
<.button variant="secondary">Close
Content
```
This example shows the main components to build a basic sheet. You can extend
this piece of code to whatever you want based on sheet.
### Using more style components
```heex
<.button>Open
Edit profile
Make changes to your profile here. Click save when you're done.
<% # example content %>
<.skeleton class="h-[125px] w-full rounded-xl" />
<.skeleton class="h-4 w-full" />
<.skeleton class="h-4 w-3/4" />
<.button>Cancel
<.button>Save
```
### A sheet as sidebar
```heex
```
## References
* [shadcn/ui - Sheet](https://ui.shadcn.com/docs/components/sheet)
"""
use Shino.UI, :component
defmodule Root do
@moduledoc false
defstruct [:id, :side]
end
@doc """
The root contains all the parts of a sheet.
"""
attr :id, :string, required: true
attr :default_open, :boolean, default: false
attr :side, :string, values: ["left", "right", "top", "bottom"], default: "left"
attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true
def root(assigns) do
~H"""
<%= render_slot(@inner_block, %Root{id: @id, side: @side}) %>
"""
end
@doc """
Renders a sheet trigger.
"""
attr :for, Root, required: true
attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true
def trigger(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders a sheet content.
"""
attr :for, Root, required: true
attr :class, :any, default: nil
slot :inner_block, required: true
def content(assigns) do
~H"""
<.focus_wrap
id={"#{@for.id}-sheet"}
role="sheet"
aria-modal="true"
phx-click-away={JS.exec("data-hide", to: "##{@for.id}")}
phx-window-keydown={JS.exec("data-hide", to: "##{@for.id}")}
phx-key="escape"
class={mc(["relative p-6 bg-background", @class])}
>
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders a sheet close.
"""
attr :for, Root, required: true
attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true
def close(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders a sheet header.
"""
attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true
def header(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders a sheet title.
"""
attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true
def title(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders a sheet description.
"""
attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true
def description(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@doc """
Renders a sheet footer.
"""
attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true
def footer(assigns) do
~H"""
<%= render_slot(@inner_block) %>
"""
end
@side_classes %{
"left" => "flex-row justify-start",
"right" => "flex-row justify-end",
"top" => "flex-col justify-start",
"bottom" => "flex-col justify-end"
}
defp side_class(side), do: Map.fetch!(@side_classes, side)
defp show_sheet(js \\ %JS{}, id, side) do
selector_content = "##{id}-content"
selector_overlay = "##{id}-overlay"
selector_sheet = "##{id}-sheet"
transition_overlay = {"transition duration-300 ease-in-out", "opacity-0", "opacity-100"}
transition_sheet =
case side do
"left" -> {"transition duration-300 ease-in-out", "-translate-x-full", "translate-x-0"}
"right" -> {"transition duration-300 ease-in-out", "translate-x-full", "translate-x-0"}
"top" -> {"transition duration-300 ease-in-out", "-translate-y-full", "translate-y-0"}
"bottom" -> {"transition duration-300 ease-in-out", "translate-y-full", "translate-y-0"}
end
js
|> JS.remove_class("hidden", to: selector_content)
|> JS.transition(transition_overlay, to: selector_overlay, time: 300)
|> JS.transition(transition_sheet, to: selector_sheet, time: 300)
|> JS.add_class("overflow-hidden", to: "body")
|> JS.focus_first(to: selector_sheet)
end
defp hide_sheet(js \\ %JS{}, id, side) do
selector_content = "##{id}-content"
selector_overlay = "##{id}-overlay"
selector_sheet = "##{id}-sheet"
transition_overlay = {"transition duration-300 ease-in-out", "opacity-100", "opacity-0"}
transition_sheet =
case side do
"left" -> {"transition duration-300 ease-in-out", "translate-x-0", "-translate-x-full"}
"right" -> {"transition duration-300 ease-in-out", "translate-x-0", "translate-x-full"}
"top" -> {"transition duration-300 ease-in-out", "translate-y-0", "-translate-y-full"}
"bottom" -> {"transition duration-300 ease-in-out", "translate-y-0", "translate-y-full"}
end
js
|> JS.add_class("hidden",
to: selector_content,
transition: {"duration-300", "", ""},
time: 300
)
|> JS.transition(transition_overlay, to: selector_overlay, time: 300)
|> JS.transition(transition_sheet, to: selector_sheet, time: 300)
|> JS.remove_class("overflow-hidden", to: "body")
|> JS.pop_focus()
end
end