Hover Card
For sighted users to preview content available behind a link.
Usage
Example
JD
@joeldrapper
Creator of Phlex Components. Ruby on Rails developer.
Joined December 2021
HoverCard do HoverCardTrigger do Button(variant: :link) { "@joeldrapper" } # Make this a link in order to navigate somewhere end HoverCardContent do div(class: "flex justify-between space-x-4") do Avatar do AvatarImage(src: "https://avatars.githubusercontent.com/u/246692?v=4", alt: "joeldrapper") AvatarFallback { "JD" } end div(class: "space-y-1") do h4(class: "text-sm font-medium") { "@joeldrapper" } p(class: "text-sm") do "Creator of Phlex Components. Ruby on Rails developer." end div(class: "flex items-center pt-2") do svg( width: "15", height: "15", viewbox: "0 0 15 15", fill: "none", xmlns: "http://www.w3.org/2000/svg", class: "mr-2 h-4 w-4 opacity-70" ) do |s| s.path( d: "M4.5 1C4.77614 1 5 1.22386 5 1.5V2H10V1.5C10 1.22386 10.2239 1 10.5 1C10.7761 1 11 1.22386 11 1.5V2H12.5C13.3284 2 14 2.67157 14 3.5V12.5C14 13.3284 13.3284 14 12.5 14H2.5C1.67157 14 1 13.3284 1 12.5V3.5C1 2.67157 1.67157 2 2.5 2H4V1.5C4 1.22386 4.22386 1 4.5 1ZM10 3V3.5C10 3.77614 10.2239 4 10.5 4C10.7761 4 11 3.77614 11 3.5V3H12.5C12.7761 3 13 3.22386 13 3.5V5H2V3.5C2 3.22386 2.22386 3 2.5 3H4V3.5C4 3.77614 4.22386 4 4.5 4C4.77614 4 5 3.77614 5 3.5V3H10ZM2 6V12.5C2 12.7761 2.22386 13 2.5 13H12.5C12.7761 13 13 12.7761 13 12.5V6H2ZM7 7.5C7 7.22386 7.22386 7 7.5 7C7.77614 7 8 7.22386 8 7.5C8 7.77614 7.77614 8 7.5 8C7.22386 8 7 7.77614 7 7.5ZM9.5 7C9.22386 7 9 7.22386 9 7.5C9 7.77614 9.22386 8 9.5 8C9.77614 8 10 7.77614 10 7.5C10 7.22386 9.77614 7 9.5 7ZM11 7.5C11 7.22386 11.2239 7 11.5 7C11.7761 7 12 7.22386 12 7.5C12 7.77614 11.7761 8 11.5 8C11.2239 8 11 7.77614 11 7.5ZM11.5 9C11.2239 9 11 9.22386 11 9.5C11 9.77614 11.2239 10 11.5 10C11.7761 10 12 9.77614 12 9.5C12 9.22386 11.7761 9 11.5 9ZM9 9.5C9 9.22386 9.22386 9 9.5 9C9.77614 9 10 9.22386 10 9.5C10 9.77614 9.77614 10 9.5 10C9.22386 10 9 9.77614 9 9.5ZM7.5 9C7.22386 9 7 9.22386 7 9.5C7 9.77614 7.22386 10 7.5 10C7.77614 10 8 9.77614 8 9.5C8 9.22386 7.77614 9 7.5 9ZM5 9.5C5 9.22386 5.22386 9 5.5 9C5.77614 9 6 9.22386 6 9.5C6 9.77614 5.77614 10 5.5 10C5.22386 10 5 9.77614 5 9.5ZM3.5 9C3.22386 9 3 9.22386 3 9.5C3 9.77614 3.22386 10 3.5 10C3.77614 10 4 9.77614 4 9.5C4 9.22386 3.77614 9 3.5 9ZM3 11.5C3 11.2239 3.22386 11 3.5 11C3.77614 11 4 11.2239 4 11.5C4 11.7761 3.77614 12 3.5 12C3.22386 12 3 11.7761 3 11.5ZM5.5 11C5.22386 11 5 11.2239 5 11.5C5 11.7761 5.22386 12 5.5 12C5.77614 12 6 11.7761 6 11.5C6 11.2239 5.77614 11 5.5 11ZM7 11.5C7 11.2239 7.22386 11 7.5 11C7.77614 11 8 11.2239 8 11.5C8 11.7761 7.77614 12 7.5 12C7.22386 12 7 11.7761 7 11.5ZM9.5 11C9.22386 11 9 11.2239 9 11.5C9 11.7761 9.22386 12 9.5 12C9.77614 12 10 11.7761 10 11.5C10 11.2239 9.77614 11 9.5 11Z", fill: "currentColor", fill_rule: "evenodd", clip_rule: "evenodd" ) end span(class: "text-xs text-muted-foreground") { "Joined December 2021" } end end end end end
Copied!
Copy failed!
Installation
Using RubyUI CLI
Run the install command
rails g ruby_ui:component HoverCard
Copied!
Copy failed!
Manual installation
1
Add RubyUI::HoverCard
to app/components/ruby_ui/hover_card.rb
# frozen_string_literal: true module RubyUI class HoverCard < Base def initialize(option: {}, **attrs) @options = option @options[:delay] ||= [500, 250] @options[:trigger] ||= "mouseenter focus click" super(**attrs) end def view_template(&) div(**attrs, &) end private def default_attrs { data: { controller: "ruby-ui--hover-card", ruby_ui__hover_card_options_value: @options.to_json } } end end end
Copied!
Copy failed!
2
Add RubyUI::HoverCardContent
to app/components/ruby_ui/hover_card/hover_card_content.rb
# frozen_string_literal: true module RubyUI class HoverCardContent < Base def view_template(&block) template(data: {ruby_ui__hover_card_target: "content"}) do div(**attrs, &block) end end private def default_attrs { data: { state: :open }, class: "z-50 rounded-md border bg-background p-4 text-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2" } end end end
Copied!
Copy failed!
3
Add RubyUI::HoverCardTrigger
to app/components/ruby_ui/hover_card/hover_card_trigger.rb
# frozen_string_literal: true module RubyUI class HoverCardTrigger < Base def view_template(&) div(**attrs, &) end private def default_attrs { data: { ruby_ui__hover_card_target: "trigger" }, class: "inline-block" } end end end
Copied!
Copy failed!
4
Add hover_card_controller.js
to app/javascript/controllers/ruby_ui/hover_card_controller.js
import { Controller } from "@hotwired/stimulus"; import tippy from "tippy.js"; export default class extends Controller { static targets = ["trigger", "content", "menuItem"]; static values = { options: { type: Object, default: {}, }, // make content width of the trigger element (true/false) matchWidth: { type: Boolean, default: false, } } connect() { this.boundHandleKeydown = this.handleKeydown.bind(this); // Bind the function so we can remove it later this.initializeTippy(); this.selectedIndex = -1; } disconnect() { this.destroyTippy(); } initializeTippy() { const defaultOptions = { content: this.contentTarget.innerHTML, allowHTML: true, interactive: true, onShow: (instance) => { this.matchWidthValue && this.setContentWidth(instance); // ensure content width matches trigger width this.addEventListeners(); }, onHide: () => { this.removeEventListeners(); this.deselectAll(); }, popperOptions: { modifiers: [ { name: "offset", options: { offset: [0, 4] }, }, ], } }; const mergedOptions = { ...this.optionsValue, ...defaultOptions }; this.tippy = tippy(this.triggerTarget, mergedOptions); } destroyTippy() { if (this.tippy) { this.tippy.destroy(); } } setContentWidth(instance) { // box-sizing: border-box const content = instance.popper.querySelector('.tippy-content'); if (content) { content.style.width = `${instance.reference.offsetWidth}px`; } } handleContextMenu(event) { event.preventDefault(); this.open(); } open() { this.tippy.show(); } close() { this.tippy.hide(); } handleKeydown(e) { // return if no menu items (one line fix for) if (this.menuItemTargets.length === 0) { return; } if (e.key === 'ArrowDown') { e.preventDefault(); this.updateSelectedItem(1); } else if (e.key === 'ArrowUp') { e.preventDefault(); this.updateSelectedItem(-1); } else if (e.key === 'Enter' && this.selectedIndex !== -1) { e.preventDefault(); this.menuItemTargets[this.selectedIndex].click(); } } updateSelectedItem(direction) { // Check if any of the menuItemTargets have aria-selected="true" and set the selectedIndex to that index this.menuItemTargets.forEach((item, index) => { if (item.getAttribute('aria-selected') === 'true') { this.selectedIndex = index; } }); if (this.selectedIndex >= 0) { this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], false); } this.selectedIndex += direction; if (this.selectedIndex < 0) { this.selectedIndex = this.menuItemTargets.length - 1; } else if (this.selectedIndex >= this.menuItemTargets.length) { this.selectedIndex = 0; } this.toggleAriaSelected(this.menuItemTargets[this.selectedIndex], true); } toggleAriaSelected(element, isSelected) { // Add or remove attribute if (isSelected) { element.setAttribute('aria-selected', 'true'); } else { element.removeAttribute('aria-selected'); } } deselectAll() { this.menuItemTargets.forEach(item => this.toggleAriaSelected(item, false)); this.selectedIndex = -1; } addEventListeners() { document.addEventListener('keydown', this.boundHandleKeydown); } removeEventListeners() { document.removeEventListener('keydown', this.boundHandleKeydown); } }
Copied!
Copy failed!
5
Update the Stimulus controllers manifest file
Importmap!
You don't need to run this command if you are using Importmap
rake stimulus:manifest:update
Copied!
Copy failed!
6
Install tippy.js
Javascript dependency
// with yarn yarn add tippy.js // with npm npm install tippy.js // with importmaps // Add to your config/importmap.rb pin "tippy.js", to: "https://cdn.jsdelivr.net/npm/tippy.js@6.3.7/+esm" pin "@popperjs/core", to: "https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.8/+esm"
Copied!
Copy failed!
Components
Component | Built using | Source |
---|---|---|
HoverCard | Phlex | |
HoverCardContent | Phlex | |
HoverCardTrigger | Phlex | |
HoverCardController | Stimulus JS |