Skip to content

Commit

Permalink
Implement tooltips
Browse files Browse the repository at this point in the history
Fixes #450
  • Loading branch information
mjgiarlo committed Feb 15, 2025
1 parent 3b57495 commit ecd39c1
Show file tree
Hide file tree
Showing 18 changed files with 138 additions and 38 deletions.
17 changes: 15 additions & 2 deletions app/assets/stylesheets/application.bootstrap.scss
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ body {

.opacity-85 {
opacity: 0.85;
}
}

i.icon-lg {
font-size: 2rem;
Expand All @@ -274,6 +274,19 @@ i.icon-xl {
font-size: 3rem;
}

.tooltip-info {
color: var(--stanford-digital-blue);
}

.tooltip-shift-right {
// NOTE: for some reason, the bootstrap tooltip wants to be about 16px to the left of where we want it.
left: 16px !important;
}

.tooltip-header {
display: inline-block;
}

.secondary-card {
min-height: 175px;
}
Expand All @@ -291,4 +304,4 @@ i.icon-xl {

fieldset.form-fieldset legend {
font-size: unset;
}
}
2 changes: 1 addition & 1 deletion app/components/contact_emails/edit_component.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<%= render Elements::Forms::TextFieldComponent.new(field_name: :email, label: helpers.t('contact_email.edit.fields.email.label'), form:, container_classes: 'mb-4') %>
<%= render Elements::Forms::TextFieldComponent.new(field_name: :email, label: helpers.t('contact_email.edit.fields.email.label'), tooltip: helpers.t('contact_email.edit.fields.email.tooltip_html'), form:, container_classes: 'mb-4') %>
3 changes: 2 additions & 1 deletion app/components/edit/tab_form/pane_component.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
<div>
<% if label || help_text || help? %>
<div class="pane-header">
<% if label %><h2 class="h4"><%= label %></h2><% end %>
<% if label %><h2 class="h4 tooltip-header"><%= label %></h2><% end %>
<%= render Elements::TooltipComponent.new(tooltip:) %>
<% if help_text %><p class="mb-0"><%= help_text || help %></p><% end %>
<%= help if help? %>
</div>
Expand Down
5 changes: 3 additions & 2 deletions app/components/edit/tab_form/pane_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ class PaneComponent < ApplicationComponent
renders_one :footer
renders_one :help

def initialize(tab_name:, label: nil, selected: false, help_text: nil)
def initialize(tab_name:, label: nil, selected: false, help_text: nil, tooltip: nil)
@tab_name = tab_name
@label = label
@selected = selected
@help_text = help_text
@tooltip = tooltip
super()
end

attr_reader :tab_name, :selected, :label, :help_text
attr_reader :tab_name, :selected, :label, :help_text, :tooltip

def classes
merge_classes(%w[tab-pane fade h-100], selected ? 'show active' : nil)
Expand Down
7 changes: 6 additions & 1 deletion app/components/edit/tab_form/tab_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@ def call
tag.div(
class: classes,
id: "#{tab_name}-tab",
data: { bs_toggle: 'tab', bs_target: "##{pane_id}", tab_error_target: 'tab' },
data: {
bs_toggle: 'tab',
bs_target: "##{pane_id}",
tab_error_target: 'tab',
action: 'click->tooltips#hideAll'
},
type: 'button',
'aria-controls': pane_id,
'aria-selected': selected?
Expand Down
5 changes: 3 additions & 2 deletions app/components/elements/forms/field_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ module Forms
class FieldComponent < ApplicationComponent
def initialize(form:, field_name:, required: false, hidden_label: false, label: nil, help_text: nil, # rubocop:disable Metrics/ParameterLists, Metrics/MethodLength
disabled: false, hidden: false, data: {}, input_data: {}, placeholder: nil, width: nil,
label_classes: [], container_classes: [], input_classes: [])
label_classes: [], container_classes: [], input_classes: [], tooltip: nil)
@form = form
@field_name = field_name
@required = required
Expand All @@ -22,11 +22,12 @@ def initialize(form:, field_name:, required: false, hidden_label: false, label:
@label_classes = label_classes
@container_classes = container_classes
@input_classes = input_classes
@tooltip = tooltip
super()
end

attr_reader :form, :field_name, :required, :help_text, :hidden_label, :label, :hidden, :disabled, :data,
:placeholder, :width, :label_classes, :input_data
:placeholder, :width, :label_classes, :input_data, :tooltip

def help_text_id
@help_text_id ||= form.field_id(field_name, 'help')
Expand Down
3 changes: 2 additions & 1 deletion app/components/elements/forms/label_component.html.erb
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
<%= form.label field_name, label_text, class: classes %>
<%= form.label field_name, label_text, class: classes %>
<%= render Elements::TooltipComponent.new(tooltip:) %>
5 changes: 3 additions & 2 deletions app/components/elements/forms/label_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,18 @@ module Forms
# Component for rendering a form label.
class LabelComponent < ApplicationComponent
def initialize(form:, field_name:, label_text: nil, default_label_class: 'form-label', hidden_label: false, # rubocop:disable Metrics/ParameterLists
classes: [])
classes: [], tooltip: nil)
@form = form
@label_text = label_text
@field_name = field_name
@hidden_label = hidden_label
@default_label_class = default_label_class
@classes = classes
@tooltip = tooltip
super()
end

attr_reader :field_name, :form
attr_reader :field_name, :form, :tooltip

def label_text
return field_name if @label_text.blank?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= tag.div class: container_classes, hidden:, data: do %>
<%= render Elements::Forms::LabelComponent.new(form:, field_name:, label_text: label, hidden_label:, classes: label_classes) %>
<%= render Elements::Forms::LabelComponent.new(form:, field_name:, tooltip:, label_text: label, hidden_label:, classes: label_classes) %>
<%= render Elements::Forms::HelpTextComponent.new(id: help_text_id, help_text:) %>
<%= form.text_field field_name, class: 'form-control', required:, aria: field_aria, disabled:, data: input_data, placeholder:, style: styles, form: form_id %>
<%= render Elements::Forms::InvalidFeedbackComponent.new(form:, field_name:) %>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<%= tag.div class: container_classes do %>
<%= render Elements::Forms::LabelComponent.new(form:, field_name:, label_text: label, hidden_label:, classes: label_classes) %>
<%= render Elements::Forms::LabelComponent.new(form:, field_name:, tooltip:, label_text: label, hidden_label:, classes: label_classes) %>
<%= render Elements::Forms::HelpTextComponent.new(id: help_text_id, help_text:) %>
<%= form.textarea field_name, class: 'form-control', required:, aria: field_aria, data:, rows: %>
<%= render Elements::Forms::InvalidFeedbackComponent.new(form:, field_name:) %>
Expand Down
44 changes: 44 additions & 0 deletions app/components/elements/tooltip_component.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Elements
# Component for rendering a tooltip.
class TooltipComponent < ApplicationComponent
def initialize(tooltip: nil)
# The tooltip can include a trailing \n that isn't rendered in the HTML but does make for ugly source markup
@tooltip = tooltip&.chomp
super()
end

def call
helpers.info_icon(
fill: true,
classes: 'px-2 tooltip-info',
data: {
bs_html: true,
bs_template: tooltip_template,
bs_toggle: 'tooltip',
bs_title: tooltip,
bs_trigger: 'click focus',
tooltips_target: 'icon'
}
)
end

private

attr_reader :tooltip

def render?
tooltip.present?
end

def tooltip_template
<<~HTML.squish
<div class="tooltip tooltip-shift-right" role="tooltip">
<div class="tooltip-arrow"></div>
<div class="tooltip-inner text-start"></div>
</div>
HTML
end
end
end
2 changes: 1 addition & 1 deletion app/javascript/controllers/tab_error_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from '@hotwired/stimulus'
import * as bootstrap from 'bootstrap' // eslint-disable-line no-unused-vars
import * as bootstrap from 'bootstrap'

export default class extends Controller {
static targets = ['tab', 'pane']
Expand Down
3 changes: 1 addition & 2 deletions app/javascript/controllers/tab_link_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from '@hotwired/stimulus'
import * as bootstrap from 'bootstrap' // eslint-disable-line no-unused-vars
import * as bootstrap from 'bootstrap'

// Switches to a tab when the link is clicked.
// Provide the #id of the tab to switch to in the href attribute of the link.
Expand All @@ -8,7 +8,6 @@ export default class extends Controller {
event.preventDefault()

const tabAnchor = this.element.getAttribute('href')
console.log('tabAnchor:', tabAnchor)
bootstrap.Tab.getOrCreateInstance(tabAnchor).show() // eslint-disable-line no-undef
}
}
2 changes: 1 addition & 1 deletion app/javascript/controllers/tab_next_controller.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Controller } from '@hotwired/stimulus'
import * as bootstrap from 'bootstrap' // eslint-disable-line no-unused-vars
import * as bootstrap from 'bootstrap'

export default class extends Controller {
static values = { selector: String }
Expand Down
26 changes: 26 additions & 0 deletions app/javascript/controllers/tooltips_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Controller } from '@hotwired/stimulus'
import * as bootstrap from 'bootstrap'

export default class extends Controller {
static targets = ['icon']

iconTargetConnected (target) {
const tooltip = new bootstrap.Tooltip(target)

target.addEventListener('show.bs.tooltip', () => this.hideAllExcept(target))

return tooltip
}

// Hide all tooltips (e.g., when switching tabs)
hideAll (_event) {
this.iconTargets.forEach(iconTarget => bootstrap.Tooltip.getInstance(iconTarget).hide())
}

// Display only one tooltip on the page at a time
hideAllExcept (target) {
this.iconTargets
.filter(iconTarget => iconTarget !== target)
.forEach(iconTarget => bootstrap.Tooltip.getInstance(iconTarget).hide())
}
}
34 changes: 17 additions & 17 deletions app/views/collections/form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,61 @@
<% end %>
<% end %>
<%= render Elements::HeadingComponent.new(level: :h1, variant: :h2, text: @collection_form.title || 'Untitled collection', classes: 'mb-3') %>
<%= render Elements::AlertComponent.new(variant: :danger, value: I18n.t('messages.validation')) if @collection_form.errors.present? %>
<%= render Elements::AlertComponent.new(variant: :danger, value: t('messages.validation')) if @collection_form.errors.present? %>
<% discard_draft_form_id = dom_id(@collection_form, 'discard_form') %>
<%= render Edit::DiscardDraftFormComponent.new(presenter: @collection_presenter, id: discard_draft_form_id) if @collection_presenter %>
<%= render Edit::TabForm::TabListComponent.new(model: @collection_form, hidden_fields: %i[lock version]) do |component| %>
<% component.with_tab(label: I18n.t('collections.edit.panes.details.tab_label'), tab_name: :details, selected: true) %>
<% component.with_tab(label: I18n.t('collections.edit.panes.related_links.tab_label'), tab_name: :related_links) %>
<% component.with_tab(label: I18n.t('collections.edit.panes.access.tab_label'), tab_name: :access) %>
<% component.with_tab(label: I18n.t('collections.edit.panes.license.tab_label'), tab_name: :license) %>
<% component.with_tab(label: I18n.t('collections.edit.panes.participants.tab_label'), tab_name: :participants) %>
<% component.with_tab(label: I18n.t('collections.edit.panes.workflow.tab_label'), tab_name: :workflow) %>
<% component.with_tab(label: I18n.t('collections.edit.panes.deposit.tab_label'), tab_name: :deposit) %>
<% component.with_tab(label: t('collections.edit.panes.details.tab_label'), tab_name: :details, selected: true) %>
<% component.with_tab(label: t('collections.edit.panes.related_links.tab_label'), tab_name: :related_links) %>
<% component.with_tab(label: t('collections.edit.panes.access.tab_label'), tab_name: :access) %>
<% component.with_tab(label: t('collections.edit.panes.license.tab_label'), tab_name: :license) %>
<% component.with_tab(label: t('collections.edit.panes.participants.tab_label'), tab_name: :participants) %>
<% component.with_tab(label: t('collections.edit.panes.workflow.tab_label'), tab_name: :workflow) %>
<% component.with_tab(label: t('collections.edit.panes.deposit.tab_label'), tab_name: :deposit) %>
<%# The panes below need a form in order to render their contents. This provides a "fake" form to use for that purpose. %>
<%# TabListComponent will render the actual form. %>
<% form_with(model: @collection_form) do |form| %>
<% component.with_pane do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :details, label: I18n.t('collections.edit.panes.details.label'), selected: true) do %>
<%= render Elements::Forms::TextFieldComponent.new(form:, field_name: :title, label: I18n.t('collections.edit.fields.title.label'), container_classes: 'pane-section') %>
<%= render Elements::Forms::TextareaFieldComponent.new(form:, field_name: :description, label: I18n.t('collections.edit.fields.description.label'), container_classes: 'pane-section') %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :details, label: t('collections.edit.panes.details.label'), selected: true) do %>
<%= render Elements::Forms::TextFieldComponent.new(form:, field_name: :title, label: t('collections.edit.fields.title.label'), tooltip: t('collections.edit.fields.title.tooltip_html'), container_classes: 'pane-section') %>
<%= render Elements::Forms::TextareaFieldComponent.new(form:, field_name: :description, label: t('collections.edit.fields.description.label'), tooltip: t('collections.edit.fields.description.tooltip_html'), container_classes: 'pane-section') %>
<%= render NestedComponentPresenter.for(form:, field_name: :contact_emails, model_class: ContactEmailForm, form_component: ContactEmails::EditComponent, hidden_label: true, bordered: false, single_field: true, fieldset_classes: 'pane-section') %>
<% end %>
<% end %>

<% component.with_pane do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :related_links, label: I18n.t('collections.edit.panes.related_links.label')) do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :related_links, label: t('collections.edit.panes.related_links.label'), tooltip: t('collections.edit.panes.related_links.tooltip_html')) do %>
<%= render NestedComponentPresenter.for(form:, field_name: :related_links, model_class: RelatedLinkForm, form_component: RelatedLinks::EditComponent, fieldset_classes: 'pane-section') %>
<% end %>
<% end %>

<% component.with_pane do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :access, label: I18n.t('collections.edit.panes.access.label')) do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :access, label: t('collections.edit.panes.access.label')) do %>
<%= render Collections::Edit::AccessSettingsComponent.new(form:) %>
<% end %>
<% end %>

<% component.with_pane do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :license, label: I18n.t('collections.edit.panes.license.label'), help_text: t('collections.edit.panes.license.help_text')) do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :license, label: t('collections.edit.panes.license.label'), help_text: t('collections.edit.panes.license.help_text')) do %>
<%= render Collections::Edit::LicenseComponent.new(form:) %>
<% end %>
<% end %>

<% component.with_pane do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :participants, label: I18n.t('collections.edit.panes.participants.label'), help_text: t('collections.edit.panes.participants.help_text')) do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :participants, label: t('collections.edit.panes.participants.label'), help_text: t('collections.edit.panes.participants.help_text')) do %>
<%= render NestedComponentPresenter.for(form:, field_name: :managers, model_class: ManagerForm, form_component: Collections::ContributorComponent, fieldset_classes: 'pane-section') %>
<%= render NestedComponentPresenter.for(form:, field_name: :depositors, model_class: DepositorForm, form_component: Collections::ContributorComponent, fieldset_classes: 'pane-section') %>
<% end %>
<% end %>

<% component.with_pane do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :workflow, label: I18n.t('collections.edit.panes.workflow.label'), help_text: t('collections.edit.panes.workflow.help_text')) do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :workflow, label: t('collections.edit.panes.workflow.label'), help_text: t('collections.edit.panes.workflow.help_text')) do %>
<%= render Collections::Edit::WorkflowComponent.new(form:) %>
<% end %>
<% end %>

<% component.with_pane do %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :deposit, label: I18n.t('collections.edit.panes.deposit.label'), help_text: t('collections.edit.panes.deposit.help_text')) do |component| %>
<%= render Collections::Edit::PaneComponent.new(tab_name: :deposit, label: t('collections.edit.panes.deposit.label'), help_text: t('collections.edit.panes.deposit.help_text')) do |component| %>
<%# For spacing %>
<div class="pane-section"></div>
<% component.with_deposit_button do %>
Expand Down
2 changes: 1 addition & 1 deletion app/views/layouts/application.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
<%= yield :breadcrumbs %>
</nav>
</div>
<main id="main-container">
<main id="main-container" data-controller="tooltips">
<% if content_for?(:fluid_body) %>
<div class="fluid-container">
<%= yield :fluid_body %>
Expand Down
12 changes: 10 additions & 2 deletions config/locales/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,13 @@ en:
fields:
email:
label: 'Contact email'
tooltip_html: >
Enter one or more valid email addresses (one per box) where people can write to ask questions about this collection. Consider using a group address that will be active and monitored for a long time to ensure that questions are received. Email addresses will appear on the public page for the collection.
validation:
email:
invalid: 'must provide a valid email address'
keyword:
edit:
keyword:
edit:
fields:
text:
label: 'Keywords (one per box)'
Expand Down Expand Up @@ -231,10 +233,14 @@ en:
help_text: >
Enter a summary statement (max 600 words) describing the collection that you are creating.
A detailed statement may improve the discovery of your collection in web searches.
tooltip_html: >
Enter an approximately 2-3 sentence description of the works to be included in this collection. You may want to include information on who the creators of the works are/will be, general subject matter of the works, types of works (technical reports, datasets, video archives, etc), and the scholarly value of the content.
title:
label: 'Collection name'
help_text: >
A unique, descriptive name may improve discovery of your collection in web searches.
tooltip_html: >
Enter a name for your collection. This name will serve as the title on the public page for the collection and the title of web search results, as well as in Stanford Libraries' catalog. Please do not use the word "Collection" at the end of the title.
access:
world: 'Everyone -- anyone in the world will be able to download the files for all deposits in this collection.'
stanford: 'Stanford Community -- only members of the Stanford community will be able to download the files for all deposits in this collection.'
Expand Down Expand Up @@ -266,6 +272,8 @@ en:
related_links:
tab_label: 'Related links (optional)'
label: 'Links to related content (optional)'
tooltip_html: >
This section is for links to other web pages that are related to your collection. Enter the text to be displayed on the page in the "Link text" box. This entire text will be hyperlinked to the URL you enter in the "URL" box. Examples of content you might consider linking to are key funder websites, lab or institute websites, or project pages.
access:
tab_label: 'Access settings'
label: 'Manage release of deposits for discovery and download'
Expand Down

0 comments on commit ecd39c1

Please sign in to comment.