Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Accessibility hook #87

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions lib/mix/tasks/salad.add.ex
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ defmodule Mix.Tasks.Salad.Add do
:ok <- File.mkdir_p(Path.dirname(target_path)),
modified_content = insert_target_module_name(source_content, file_name),
:ok <- File.write(target_path, modified_content),
:ok <- setup_zag_integration(source_content, file_name),
:ok <- maybe_perform_additional_setup(file_name) do
Mix.shell().info("#{file_name} component installed successfully ✅")
else
Expand Down Expand Up @@ -131,6 +132,36 @@ defmodule Mix.Tasks.Salad.Add do
:ok
end

defp setup_zag_integration(source, file_name) do
# grab the component we are targeting in Zag
case Regex.run(~r/data-component="([^"]+)"/, source) do
[_, target_zag_component] ->
zag_imports_path = Path.join(File.cwd!(), "assets/js/zag/index.js")
export_statement = "export * as #{target_zag_component} from \"@zag-js/#{target_zag_component}\";\n"

existing_content = File.read!(zag_imports_path)

unless String.contains?(existing_content, export_statement) do
File.write!(zag_imports_path, export_statement, [:append])
end

unless Mix.env() == :test do
Mix.shell().cmd("npm install @zag-js/#{target_zag_component} --prefix assets")
end

:ok

_ ->
Mix.shell().info("""
The component you are trying to install (#{file_name}) does not have a data-component attribute set, so you cannot use Zag with it. The component will lack accessibility support and interactive features
""")

continue? = Mix.shell().yes?("Do you want to continue with the installation?")

if continue?, do: :ok, else: {:error, "Installation aborted"}
end
end

defp get_module_name do
Mix.Project.config()[:app]
|> Atom.to_string()
Expand Down
20 changes: 20 additions & 0 deletions lib/mix/tasks/salad.init.ex
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ defmodule Mix.Tasks.Salad.Init do
:ok <- patch_css(color_scheme, assets_path),
:ok <- patch_js(assets_path),
:ok <- copy_tailwind_colors(assets_path),
:ok <- copy_zag_files(assets_path),
:ok <- patch_tailwind_config(opts),
:ok <- maybe_write_helpers_module(component_path, app_name, opts),
:ok <- maybe_write_component_module(component_path, app_name, opts),
Expand Down Expand Up @@ -155,6 +156,25 @@ defmodule Mix.Tasks.Salad.Init do
:ok
end

defp copy_zag_files(assets_path) do
Mix.shell().info("Copying zag files to assets folder")
source_path = Path.join(assets_path, "zag")
target_path = Path.join(File.cwd!(), "assets/js/zag")

File.mkdir_p!(target_path)

Enum.each(File.ls!(source_path), fn file ->
unless File.exists?(Path.join(target_path, file)) do
File.cp!(Path.join(source_path, file), Path.join(target_path, file))
end
end)

Mix.shell().info("\nZagHook installed successfully")
Mix.shell().info("Do not forget to import it into your app.js file and pass it to your live socket")

:ok
end

defp patch_tailwind_config(opts) do
Mix.shell().info("Patching tailwind.config.js")
tailwind_config_path = Path.join(File.cwd!(), "assets/tailwind.config.js")
Expand Down
34 changes: 16 additions & 18 deletions lib/salad_ui/collapsible.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,26 @@ defmodule SaladUI.Collapsible do

attr :id, :string,
required: true,
doc: "Id to identify collapsible component, collapsible_trigger uses this id to toggle content visibility"
doc: "Id to identify collapsible component"

attr :open, :boolean, default: true, doc: "Initial state of collapsible content"
attr :open, :boolean, default: false, doc: "Initial state of collapsible content"
attr :listeners, :list, default: []
attr :class, :string, default: nil
attr :rest, :global, include: ~w(title)
slot(:inner_block, required: true)

def collapsible(assigns) do
assigns =
assign(assigns, :open, normalize_boolean(assigns[:open]))
assigns = assign(assigns, :open, normalize_boolean(assigns[:open]))

~H"""
<div
data-state="closed"
phx-toggle-collapsible={toggle_collapsible(@id)}
phx-mounted={@open && JS.exec("phx-toggle-collapsible", to: "##{@id}")}
class={classes(["inline-block relative collapsible-root", @class])}
data-component="collapsible"
data-parts={Jason.encode!(["trigger", "content"])}
data-options={Jason.encode!(%{open: :bool})}
data-open={@open}
data-listeners={Jason.encode!(@listeners)}
phx-hook="ZagHook"
class={classes(["inline-block relative", @class])}
id={@id}
{@rest}
>
Expand All @@ -55,12 +58,7 @@ defmodule SaladUI.Collapsible do

def collapsible_trigger(assigns) do
~H"""
<.dynamic
tag={@as_tag}
onclick={exec_closest("phx-toggle-collapsible", ".collapsible-root")}
class={@class}
{@rest}
>
<div data-part="trigger" class={@class}>
<%= render_slot(@inner_block) %>
</.dynamic>
"""
Expand All @@ -76,9 +74,10 @@ defmodule SaladUI.Collapsible do
def collapsible_content(assigns) do
~H"""
<div
data-part="content"
class={
classes([
"collapsible-content hidden transition-all duration-200 ease-in-out",
"transition-all duration-200 ease-in-out",
@class
])
}
Expand All @@ -93,9 +92,8 @@ defmodule SaladUI.Collapsible do
Show collapsible content.
"""
def toggle_collapsible(js \\ %JS{}, id) do
js
|> JS.toggle(
to: "##{id} .collapsible-content",
JS.toggle(js,
to: "##{id} [data-part='content']",
in: {"ease-out duration-200", "opacity-0", "opacity-100"},
out: {"ease-out", "opacity-100", "opacity-70"},
time: 200
Expand Down
6 changes: 6 additions & 0 deletions lib/salad_ui/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ defmodule SaladUI.Helpers do
"#{shared_classes} #{variation_classes}"
end

def unique_id(seed \\ 16, length \\ 22) do
seed
|> :crypto.strong_rand_bytes()
|> Base.url_encode64()
|> binary_part(0, length)
end
@doc """
Common function for building variant

Expand Down
Loading