accessibility and poller updates

This commit is contained in:
Graham McIntire 2026-01-26 13:02:03 -06:00
parent 902d21eeee
commit 06fde717c0
No known key found for this signature in database
5 changed files with 86 additions and 13 deletions

View file

@ -17,7 +17,7 @@ defmodule ToweropsWeb do
those modules here.
"""
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt)
def static_paths, do: ~w(assets fonts images favicon.ico robots.txt changelog.txt)
def router do
quote do

View file

@ -250,9 +250,11 @@ defmodule ToweropsWeb.CoreComponents do
def input(%{type: "checkbox"} = assigns) do
assigns =
assign_new(assigns, :checked, fn ->
assigns
|> assign_new(:checked, fn ->
Form.normalize_value("checkbox", assigns[:value])
end)
|> assign_new(:error_id, fn -> assigns.id && "#{assigns.id}-error" end)
~H"""
<div class="mb-4">
@ -270,6 +272,8 @@ defmodule ToweropsWeb.CoreComponents do
name={@name}
value="true"
checked={@checked}
aria-describedby={@errors != [] && @error_id}
aria-invalid={to_string(@errors != [])}
class={
@class ||
"h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-800"
@ -278,12 +282,16 @@ defmodule ToweropsWeb.CoreComponents do
/>
<span class="text-sm font-medium text-gray-900 dark:text-white">{@label}</span>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
<.error :if={@errors != []} id={@error_id}>
<span :for={msg <- @errors}>{msg}</span>
</.error>
</div>
"""
end
def input(%{type: "select"} = assigns) do
assigns = assign_new(assigns, :error_id, fn -> assigns.id && "#{assigns.id}-error" end)
~H"""
<div class="mb-4">
<label :if={@label} for={@id} class="block text-sm/6 font-medium text-gray-900 dark:text-white">
@ -293,6 +301,8 @@ defmodule ToweropsWeb.CoreComponents do
<select
id={@id}
name={@name}
aria-describedby={@errors != [] && @error_id}
aria-invalid={to_string(@errors != [])}
class={[
@class ||
"col-start-1 row-start-1 w-full appearance-none rounded-md bg-white py-1.5 pr-8 pl-3 text-base text-gray-900 outline-1 -outline-offset-1 placeholder:text-gray-400 focus:outline-2 focus:-outline-offset-2 sm:text-sm/6 dark:bg-white/5 dark:text-white dark:outline-white/10 dark:*:bg-gray-800",
@ -322,12 +332,16 @@ defmodule ToweropsWeb.CoreComponents do
/>
</svg>
</div>
<.error :for={msg <- @errors}>{msg}</.error>
<.error :if={@errors != []} id={@error_id}>
<span :for={msg <- @errors}>{msg}</span>
</.error>
</div>
"""
end
def input(%{type: "textarea"} = assigns) do
assigns = assign_new(assigns, :error_id, fn -> assigns.id && "#{assigns.id}-error" end)
~H"""
<div class="mb-4">
<label>
@ -337,6 +351,8 @@ defmodule ToweropsWeb.CoreComponents do
<textarea
id={@id}
name={@name}
aria-describedby={@errors != [] && @error_id}
aria-invalid={to_string(@errors != [])}
class={[
@class ||
"block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-gray-800 dark:text-white",
@ -349,13 +365,17 @@ defmodule ToweropsWeb.CoreComponents do
{@rest}
>{Phoenix.HTML.Form.normalize_value("textarea", @value)}</textarea>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
<.error :if={@errors != []} id={@error_id}>
<span :for={msg <- @errors}>{msg}</span>
</.error>
</div>
"""
end
# All other inputs text, datetime-local, url, password, etc. are handled here...
def input(assigns) do
assigns = assign_new(assigns, :error_id, fn -> assigns.id && "#{assigns.id}-error" end)
~H"""
<div class="mb-4">
<label>
@ -367,6 +387,8 @@ defmodule ToweropsWeb.CoreComponents do
name={@name}
id={@id}
value={Phoenix.HTML.Form.normalize_value(@type, @value)}
aria-describedby={@errors != [] && @error_id}
aria-invalid={to_string(@errors != [])}
class={[
@class ||
"block w-full rounded-lg border-0 py-2 px-3 text-gray-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset sm:text-sm sm:leading-6 dark:bg-gray-800 dark:text-white",
@ -379,7 +401,9 @@ defmodule ToweropsWeb.CoreComponents do
{@rest}
/>
</label>
<.error :for={msg <- @errors}>{msg}</.error>
<.error :if={@errors != []} id={@error_id}>
<span :for={msg <- @errors}>{msg}</span>
</.error>
</div>
"""
end

View file

@ -142,15 +142,19 @@ defmodule ToweropsWeb.Layouts do
:if={@current_organization}
type="button"
id="mobile-menu-button"
aria-expanded="false"
aria-controls="mobile-menu"
phx-click={
JS.toggle(to: "#mobile-menu")
|> JS.toggle(to: "#mobile-menu-icon-open")
|> JS.toggle(to: "#mobile-menu-icon-close")
|> JS.toggle_attribute({"aria-expanded", "true", "false"},
to: "#mobile-menu-button"
)
}
class="inline-flex items-center justify-center rounded-md p-2 text-gray-500 hover:bg-gray-100 hover:text-gray-700 focus:outline-none dark:text-gray-400 dark:hover:bg-gray-800 dark:hover:text-gray-200 md:hidden"
aria-expanded="false"
>
<span class="sr-only">Open main menu</span>
<span class="sr-only">Toggle main menu</span>
<span id="mobile-menu-icon-open"><.icon name="hero-bars-3" class="size-6" /></span>
<span id="mobile-menu-icon-close" class="hidden">
<.icon name="hero-x-mark" class="size-6" />
@ -215,7 +219,13 @@ defmodule ToweropsWeb.Layouts do
<button
type="button"
id="org-menu-button"
phx-click={JS.toggle(to: "#org-menu")}
aria-expanded="false"
aria-haspopup="true"
aria-controls="org-menu"
phx-click={
JS.toggle(to: "#org-menu")
|> JS.toggle_attribute({"aria-expanded", "true", "false"}, to: "#org-menu-button")
}
class="inline-flex items-center justify-center gap-x-1 rounded-md bg-white px-2 py-1.5 sm:px-3 sm:py-2 text-xs sm:text-sm font-semibold text-gray-900 shadow-xs inset-ring-1 inset-ring-gray-300 hover:bg-gray-50 dark:bg-white/10 dark:text-white dark:shadow-none dark:inset-ring-white/5 dark:hover:bg-white/20"
>
<span class="hidden sm:inline">
@ -243,12 +253,17 @@ defmodule ToweropsWeb.Layouts do
<div
id="org-menu"
role="menu"
class="hidden absolute top-full right-0 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg outline-1 outline-black/5 z-50 dark:bg-gray-800 dark:shadow-none dark:-outline-offset-1 dark:outline-white/10"
phx-click-away={JS.hide(to: "#org-menu")}
phx-click-away={
JS.hide(to: "#org-menu")
|> JS.set_attribute({"aria-expanded", "false"}, to: "#org-menu-button")
}
>
<div class="py-1">
<.link
:if={@current_organization}
role="menuitem"
navigate={~p"/orgs"}
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-hidden dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
phx-click={JS.hide(to: "#org-menu")}
@ -257,6 +272,7 @@ defmodule ToweropsWeb.Layouts do
</.link>
<.link
:if={@current_organization}
role="menuitem"
navigate={~p"/orgs/#{@current_organization.slug}/settings"}
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-hidden dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
phx-click={JS.hide(to: "#org-menu")}
@ -265,6 +281,7 @@ defmodule ToweropsWeb.Layouts do
</.link>
<%= if @current_scope && @current_scope.user && @current_scope.user.is_superuser do %>
<.link
role="menuitem"
navigate={~p"/admin"}
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-hidden dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
phx-click={JS.hide(to: "#org-menu")}
@ -273,6 +290,7 @@ defmodule ToweropsWeb.Layouts do
</.link>
<% end %>
<.link
role="menuitem"
navigate={~p"/users/settings"}
class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-hidden dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
phx-click={JS.hide(to: "#org-menu")}
@ -280,6 +298,7 @@ defmodule ToweropsWeb.Layouts do
<.icon name="hero-user-circle" class="w-4 h-4" /> User Settings
</.link>
<.link
role="menuitem"
href={~p"/users/log-out"}
method="delete"
class="block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-900 focus:outline-hidden dark:text-gray-300 dark:hover:bg-white/5 dark:hover:text-white"
@ -329,7 +348,7 @@ defmodule ToweropsWeb.Layouts do
</div>
</nav>
<main class="flex-1 py-6 sm:py-10">
<main id="main-content" tabindex="-1" class="flex-1 py-6 sm:py-10">
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
{render_slot(@inner_block)}
</div>
@ -440,7 +459,13 @@ defmodule ToweropsWeb.Layouts do
<button
type="button"
id="theme-menu-button"
phx-click={JS.toggle(to: "#theme-menu")}
aria-expanded="false"
aria-haspopup="true"
aria-controls="theme-menu"
phx-click={
JS.toggle(to: "#theme-menu")
|> JS.toggle_attribute({"aria-expanded", "true", "false"}, to: "#theme-menu-button")
}
class="flex items-center justify-center p-2 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-800"
aria-label="Toggle theme"
>
@ -450,12 +475,17 @@ defmodule ToweropsWeb.Layouts do
<div
id="theme-menu"
role="menu"
class="hidden absolute top-full right-0 mt-2 w-36 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black/5 z-50 dark:bg-gray-800 dark:ring-white/10"
phx-click-away={JS.hide(to: "#theme-menu")}
phx-click-away={
JS.hide(to: "#theme-menu")
|> JS.set_attribute({"aria-expanded", "false"}, to: "#theme-menu-button")
}
>
<div class="py-1">
<button
type="button"
role="menuitem"
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
phx-click={JS.dispatch("phx:set-theme") |> JS.hide(to: "#theme-menu")}
data-phx-theme="system"
@ -464,6 +494,7 @@ defmodule ToweropsWeb.Layouts do
</button>
<button
type="button"
role="menuitem"
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
phx-click={JS.dispatch("phx:set-theme") |> JS.hide(to: "#theme-menu")}
data-phx-theme="light"
@ -472,6 +503,7 @@ defmodule ToweropsWeb.Layouts do
</button>
<button
type="button"
role="menuitem"
class="flex w-full items-center gap-2 px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-700"
phx-click={JS.dispatch("phx:set-theme") |> JS.hide(to: "#theme-menu")}
data-phx-theme="dark"
@ -619,6 +651,13 @@ defmodule ToweropsWeb.Layouts do
>
Privacy Policy
</.link>
<span class="mx-2">·</span>
<.link
href="/changelog.txt"
class="hover:text-gray-900 dark:hover:text-gray-200 underline decoration-dotted underline-offset-2"
>
Changelog
</.link>
</p>
<p class="text-xs">
Last deploy <span title={@formatted_time}>{@time_ago}</span> · {@formatted_time}

View file

@ -48,6 +48,12 @@
</script>
</head>
<body>
<a
href="#main-content"
class="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 focus:z-50 focus:p-4 focus:bg-blue-600 focus:text-white focus:rounded-md focus:m-2"
>
Skip to main content
</a>
{@inner_content}
</body>
</html>

View file

@ -0,0 +1,4 @@
2025-01-26
* Cloud poller improvements
* Accessability improved