# frozen_string_literal: true module Ariadne # `Tooltip` only appears on mouse hover or keyboard focus and contain a label or description text. # Use tooltips sparingly and as a last resort. # # When using a tooltip, follow the provided guidelines to avoid accessibility issues. # # - Tooltip text should be brief and to the point. The tooltip content must be a string. # - Tooltips should contain only **non-essential text**. Tooltips can easily be missed and are not accessible on touch devices so never # use tooltips to convey critical information. # # @accessibility # - **Never set tooltips on static elements.** Tooltips should only be used on interactive elements like buttons or links to avoid excluding keyboard-only users # and screen reader users. # - Place `Tooltip` adjacent after its trigger element in the DOM. This allows screen reader users to navigate to and copy the tooltip # content. # ### Which `type` should I set? # Setting `:description` establishes an `aria-describedby` relationship, while `:label` establishes an `aria-labelledby` relationship between the trigger element and the tooltip, # # The `type` drastically changes semantics and screen reader behavior so follow these guidelines carefully: # - When there is already a visible label text on the trigger element, the tooltip is likely intended to supplement the existing text, so set `type: :description`. # The majority of tooltips will fall under this category. # - When there is no visible text on the trigger element and the tooltip content is appropriate as a label for the element, set `type: :label`. # This type is usually only appropriate for an icon-only control. class TooltipComponent < Ariadne::Component DEFAULT_TAG = :tooltip DEFAULT_PLACEMENT = :top VALID_PLACEMENTS = [DEFAULT_PLACEMENT, :right, :bottom, :left].freeze DEFAULT_CLASSES = "ariadne-invisible ariadne-absolute ariadne-bg-slate-900 ariadne-text-white ariadne-font-semibold ariadne-max-w-xs ariadne-py-1 ariadne-px-2 ariadne-rounded z-max" DATA_CONTROLLER = "tooltip-component" DATA_ACTION = "mouseover->tooltip-component#show mouseout->tooltip-component#hide" TYPE_DEFAULT = :description TYPE_OPTIONS = [:label, TYPE_DEFAULT].freeze # DEFAULT_DATA_ATTRIBUTES = { # "data-controller": DATA_CONTROLLER, # "data-action": "mouseover->tooltip-component#show mouseout->tooltip-component#hide", # "data-tooltip-component-placement": DEFAULT_PLACEMENT, # } # @example As a description for an icon-only button # @description # If the tooltip content provides supplementary description, set `type: :description` to establish an `aria-describedby` relationship. # The trigger element should also have a _concise_ accessible label via `aria-label`. # @code # <%= render(Ariadne::HeroiconComponent.new(icon: :moon, variant: HeroiconsHelper::Icon::VARIANT_OUTLINE, attributes: { id: "bold-button-0" })) %> # <%= render(Ariadne::TooltipComponent.new(for_id: "bold-button-0", type: :description, text: "Add bold text", direction: :top)) %> # @example As a label for an icon-only button # @description # If the tooltip labels the icon-only button, set `type: :label`. This tooltip content becomes the accessible name for the button. # @code # <%= render(Ariadne::ButtonComponent.new(attributes: { id: "like-button" })) { "👍" } %> # <%= render(Ariadne::TooltipComponent.new(for_id: "like-button", type: :label, text: "Like", direction: :top)) %> # # @example As a description for a button with visible label # @description # If the button already has visible label text, the tooltip content is likely supplementary so set `type: :description`. # @code # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "save-button"}, scheme: :success)) { "Save" } %> # <%= render(Ariadne::TooltipComponent.new(for_id: "save-button", type: :description, text: "This will immediately impact all organization members", direction: :right)) %> # @example With direction # @description # Set direction of tooltip with `direction`. The tooltip is responsive and will automatically adjust direction to avoid cutting off. # @code # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "North", m: 2})) { "North" } %> # <%= render(Ariadne::TooltipComponent.new(for_id: "North", type: :description, text: "This is a North-facing tooltip, and is responsive.", direction: :top)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "South", m: 2})) { "South" } %> # <%= render(Ariadne::TooltipComponent.new(for_id: "South", type: :description, text: "This is a South-facing tooltip and is responsive.", direction: :bottom)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "East", m: 2})) { "East" } %> # <%= render(Ariadne::TooltipComponent.new(for_id: "East", type: :description, text: "This is a East-facing tooltip and is responsive.", direction: :right)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "West", m: 2})) { "West" } %> # <%= render(Ariadne::TooltipComponent.new(for_id: "West""", type: :description, text: "This is a West-facing tooltip and is responsive.", direction: :left)) %> # @example With relative parent # @description # When the tooltip and trigger element have a parent container with `relative: position`, it should not affect width of the tooltip. # @code # # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "test-button"}, scheme: :info)) { "Test" } %> # <%= render(Ariadne::TooltipComponent.new(for_id: "test-button", type: :description, text: "This tooltip should take up the full width", direction: :bottom)) %> # # @param tag [Symbol, String] The rendered tag name # @param for_id [String] The ID of the element that the tooltip should be attached to. # @param text [String] The text content of the tooltip. This should be brief and no longer than a sentence. # @param type [Symbol] <%= one_of(Ariadne::TooltipComponent::TYPE_OPTIONS) %> # @param direction [Symbol] <%= one_of(Ariadne::TooltipComponent::VALID_PLACEMENTS) %> # @param classes [String] <%= link_to_classes_docs %> # @param attributes [Hash] <%= link_to_attributes_docs %> def initialize(tag: DEFAULT_TAG, for_id:, text:, type: TYPE_DEFAULT, direction: DEFAULT_PLACEMENT, classes: "", attributes: {}) raise TypeError, "tooltip text must be a string" unless text.is_a?(String) @tag = check_incoming_tag(DEFAULT_TAG, tag) @text = text @classes = class_names(DEFAULT_CLASSES, classes) @attributes = attributes @attributes[:for] = for_id @attributes[:"data-tooltip-component-placement"] = fetch_or_raise(VALID_PLACEMENTS, direction) @attributes[:"data-type"] = fetch_or_raise(TYPE_OPTIONS, type) @attributes[:"data-tooltip-component-target"] = "tooltip" end end end