# 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 DIRECTION_DEFAULT = :s DIRECTION_OPTIONS = [DIRECTION_DEFAULT, :n, :e, :w, :ne, :nw, :se, :sw].freeze TYPE_DEFAULT = :description TYPE_OPTIONS = [:label, TYPE_DEFAULT].freeze # @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(attributes: { for: "bold-button-0" }, type: :description, text: "Add bold text", direction: :ne)) %> # @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(attributes: { for: "like-button" }, type: :label, text: "Like", direction: :n)) %> # # @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(attributes: { for: "save-button"}, type: :description, text: "This will immediately impact all organization members", direction: :ne)) %> # @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(attributes: { for: "North"}, type: :description, text: "This is a North-facing tooltip, and is responsive.", direction: :n)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "South", m: 2})) { "South" } %> # <%= render(Ariadne::TooltipComponent.new(attributes: { for: "South"}, type: :description, text: "This is a South-facing tooltip and is responsive.", direction: :s)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "East", m: 2})) { "East" } %> # <%= render(Ariadne::TooltipComponent.new(attributes: { for: "East"}, type: :description, text: "This is a East-facing tooltip and is responsive.", direction: :e)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "West", m: 2})) { "West" } %> # <%= render(Ariadne::TooltipComponent.new(attributes: { for: "West"}, type: :description, text: "This is a West-facing tooltip and is responsive.", direction: :w)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Northeast", m: 2})) { "Northeast" } %> # <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Northeast"}, type: :description, text: "This is a Northeast-facing tooltip and is responsive.", direction: :ne)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Southeast", m: 2})) { "Southeast" } %> # <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Southeast"}, type: :description, text: "This is a Southeast-facing tooltip and is responsive.", direction: :se)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Northwest", m: 2})) { "Northwest" } %> # <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Northwest"}, type: :description, text: "This is a Northwest-facing tooltip and is responsive.", direction: :nw)) %> # <%= render(Ariadne::ButtonComponent.new(attributes: {id: "Southwest", m: 2})) { "Southwest" } %> # <%= render(Ariadne::TooltipComponent.new(attributes: { for: "Southwest"}, type: :description, text: "This is a Southwest-facing tooltip and is responsive.", direction: :sw)) %> # @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(attributes: { for: "test-button" }, type: :description, text: "This tooltip should take up the full width", direction: :ne)) %> # # @param tag [Symbol, String] The rendered tag name # @param type [Symbol] <%= one_of(Ariadne::TooltipComponent::TYPE_OPTIONS) %> # @param text [String] The text content of the tooltip. This should be brief and no longer than a sentence. # @param direction [Symbol] <%= one_of(Ariadne::TooltipComponent::DIRECTION_OPTIONS) %> # @param classes [String] <%= link_to_classes_docs %> # @param attributes [Hash] <%= link_to_attributes_docs %> def initialize(tag: :"tool-tip", type: TYPE_DEFAULT, text:, direction: DIRECTION_DEFAULT, classes: "", attributes: {}) raise TypeError, "tooltip text must be a string" unless text.is_a?(String) @tag = check_incoming_tag(:"tool-tip", tag) @text = text @classes = classes @attributes = attributes @attributes[:hidden] = true @attributes[:visible] ||= false @attributes[:"data-direction"] = fetch_or_raise(DIRECTION_OPTIONS, direction) @attributes[:"data-type"] = fetch_or_raise(TYPE_OPTIONS, type) end def call render(Ariadne::BaseComponent.new(tag: @tag, classes: @classes, attributes: @attributes)) { @text } end end end