`
+ ancestor — because `focusin` only bubbles UP, a listener on
+ this descendant span would never fire when the ancestor
+ disclosure receives focus. %>
+
+ <%= helpers.icon icon_name, size: size, color: color %>
+
+ <% end %>
-
-
+
+ <%# `max-w-[20rem]` scales with the root font-size so AT users with
+ a larger base font don't see the tooltip clipped. %>
+
<%= tooltip_content %>
diff --git a/app/components/DS/tooltip.rb b/app/components/DS/tooltip.rb
index 7189c4f35..757f77bcf 100644
--- a/app/components/DS/tooltip.rb
+++ b/app/components/DS/tooltip.rb
@@ -1,7 +1,26 @@
class DS::Tooltip < ApplicationComponent
- attr_reader :placement, :offset, :cross_axis, :icon_name, :size, :color
+ AS_OPTIONS = %i[button span].freeze
+
+ attr_reader :placement, :offset, :cross_axis, :icon_name, :size, :color, :tooltip_id, :as
+
+ # NOTE: tooltip content must be non-interactive — no buttons, links,
+ # or form controls inside. Tooltips are exposed via `aria-describedby`,
+ # which announces the content as a description but does not expose
+ # interactive descendants to AT. Use a popover/menu primitive when
+ # the surface needs to host actions.
+ #
+ # `as:` controls the trigger element.
+ # :button (default) — renders `
<% elsif brex_item.sync_error.present? %>
- <%= render DS::Tooltip.new(text: brex_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: brex_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% else %>
diff --git a/app/views/ibkr_items/_ibkr_item.html.erb b/app/views/ibkr_items/_ibkr_item.html.erb
index 511550b5e..905eda0dd 100644
--- a/app/views/ibkr_items/_ibkr_item.html.erb
+++ b/app/views/ibkr_items/_ibkr_item.html.erb
@@ -32,7 +32,7 @@
<% elsif ibkr_item.sync_error.present? %>
- <%= render DS::Tooltip.new(text: ibkr_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: ibkr_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% else %>
diff --git a/app/views/indexa_capital_items/_indexa_capital_item.html.erb b/app/views/indexa_capital_items/_indexa_capital_item.html.erb
index e6f608e33..7f90af7bd 100644
--- a/app/views/indexa_capital_items/_indexa_capital_item.html.erb
+++ b/app/views/indexa_capital_items/_indexa_capital_item.html.erb
@@ -34,7 +34,7 @@
<% elsif indexa_capital_item.sync_error.present? %>
- <%= render DS::Tooltip.new(text: indexa_capital_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: indexa_capital_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% else %>
diff --git a/app/views/lunchflow_items/_lunchflow_item.html.erb b/app/views/lunchflow_items/_lunchflow_item.html.erb
index a1aec313b..0b0fecc76 100644
--- a/app/views/lunchflow_items/_lunchflow_item.html.erb
+++ b/app/views/lunchflow_items/_lunchflow_item.html.erb
@@ -35,7 +35,7 @@
<% elsif lunchflow_item.sync_error.present? %>
- <%= render DS::Tooltip.new(text: lunchflow_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: lunchflow_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% else %>
diff --git a/app/views/mercury_items/_mercury_item.html.erb b/app/views/mercury_items/_mercury_item.html.erb
index 0159551e8..6485da548 100644
--- a/app/views/mercury_items/_mercury_item.html.erb
+++ b/app/views/mercury_items/_mercury_item.html.erb
@@ -35,7 +35,7 @@
<% elsif mercury_item.sync_error.present? %>
- <%= render DS::Tooltip.new(text: mercury_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: mercury_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% else %>
diff --git a/app/views/simplefin_items/_simplefin_item.html.erb b/app/views/simplefin_items/_simplefin_item.html.erb
index c6e90e810..d09d9b202 100644
--- a/app/views/simplefin_items/_simplefin_item.html.erb
+++ b/app/views/simplefin_items/_simplefin_item.html.erb
@@ -52,7 +52,7 @@
<% if has_warnings %>
<% if stats["accounts_skipped"].to_i > 0 %>
- <%= render DS::Tooltip.new(text: t(".accounts_skipped_tooltip"), icon: "alert-triangle", size: "sm", color: "warning") %>
+ <%= render DS::Tooltip.new(text: t(".accounts_skipped_tooltip"), icon: "alert-triangle", size: "sm", color: "warning", as: :span) %>
<%= t(".accounts_skipped_label", count: stats["accounts_skipped"].to_i) %>
<% end %>
@@ -63,14 +63,15 @@
text: (ago ? t(".rate_limited_ago", time: ago) : t(".rate_limited_recently")),
icon: "clock",
size: "sm",
- color: "warning"
+ color: "warning",
+ as: :span
) %>
<% end %>
<% if stats["total_errors"].to_i > 0 || (stats["errors"].is_a?(Array) && stats["errors"].any?) %>
<% tooltip_text = simplefin_error_tooltip(stats) %>
<% if tooltip_text.present? %>
- <%= render DS::Tooltip.new(text: tooltip_text, icon: "alert-octagon", size: "sm", color: "warning") %>
+ <%= render DS::Tooltip.new(text: tooltip_text, icon: "alert-octagon", size: "sm", color: "warning", as: :span) %>
<% end %>
<% end %>
@@ -119,7 +120,7 @@
<% elsif simplefin_item.sync_error.present? && !duplicate_only_errors %>
- <%= render DS::Tooltip.new(text: simplefin_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: simplefin_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% elsif duplicate_only_errors %>
diff --git a/app/views/snaptrade_items/_snaptrade_item.html.erb b/app/views/snaptrade_items/_snaptrade_item.html.erb
index f36712ec2..636d09dbe 100644
--- a/app/views/snaptrade_items/_snaptrade_item.html.erb
+++ b/app/views/snaptrade_items/_snaptrade_item.html.erb
@@ -42,7 +42,7 @@
<% elsif snaptrade_item.sync_error.present? %>
- <%= render DS::Tooltip.new(text: snaptrade_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: snaptrade_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% else %>
diff --git a/app/views/sophtron_items/_sophtron_item.html.erb b/app/views/sophtron_items/_sophtron_item.html.erb
index a8c71d900..d91450a86 100644
--- a/app/views/sophtron_items/_sophtron_item.html.erb
+++ b/app/views/sophtron_items/_sophtron_item.html.erb
@@ -41,7 +41,7 @@
<% elsif sophtron_item.sync_error.present? %>
- <%= render DS::Tooltip.new(text: sophtron_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive") %>
+ <%= render DS::Tooltip.new(text: sophtron_item.sync_error, icon: "alert-circle", size: "sm", color: "destructive", as: :span) %>
<%= tag.span t(".error"), class: "text-destructive" %>
<% else %>
diff --git a/config/locales/views/components/en.yml b/config/locales/views/components/en.yml
index 8c49e0461..11d896222 100644
--- a/config/locales/views/components/en.yml
+++ b/config/locales/views/components/en.yml
@@ -89,6 +89,8 @@ en:
default_label: Preview
dialog:
close: Close
+ tooltip:
+ trigger_label: More info
link:
opens_in_new_tab: (opens in new tab)
provider_sync_summary:
diff --git a/test/components/previews/tooltip_component_preview.rb b/test/components/previews/tooltip_component_preview.rb
index 68bd6c320..a6345ef0b 100644
--- a/test/components/previews/tooltip_component_preview.rb
+++ b/test/components/previews/tooltip_component_preview.rb
@@ -6,7 +6,8 @@ class TooltipComponentPreview < ViewComponent::Preview
# @param icon text
# @param size select [xs, sm, md, lg, xl, 2xl]
# @param color select [default, white, success, warning, destructive, current]
- def default(text: "This is helpful information", placement: "top", offset: 10, cross_axis: 0, icon: "info", size: "sm", color: "default")
+ # @param as select [button, span]
+ def default(text: "This is helpful information", placement: "top", offset: 10, cross_axis: 0, icon: "info", size: "sm", color: "default", as: "button")
render DS::Tooltip.new(
text: text,
placement: placement,
@@ -14,7 +15,8 @@ class TooltipComponentPreview < ViewComponent::Preview
cross_axis: cross_axis,
icon: icon,
size: size,
- color: color
+ color: color,
+ as: as.to_sym
)
end