Replace initialjs-rails with custom avatar code

The initialjs-rails gem hasn't been maintained for years, and it
currently requires `railties < 7.0`, meaning we can't upgrade to Rails 7
while we depend on it.

Since the code in the gem is simple, and we were already rewriting its
most complex part (generating a background color), we can implement the
same code, only we're using Ruby instead of JavaScript. This way, the
avatars will be shown on browsers without JavaScript as well. Since
we're adding a component test that checks SVG images are displayed even
without JavaScript, we no longer need the test that checked images were
displayed after AJAX requests.

Now the tests show the user experience better; people don't care about
the internal name used to select the initial (which is what we were
checking); they care about the initial actually displayed.

Note initialjs generated an <img> tag using a `src="data:image/svg+xml;`
attribute. We're generating an <svg> tag instead, because it's easier.
For this reason, we need to change the code slightly, giving the <svg>
tag the `img` role and using `aria-label` so its contents won't be read
aloud by screen readers. We could give it a `presentation` role instead
and forget about `aria-label`, but then screen readers would read the
text anyway (or, at least, some of them would).
This commit is contained in:
Javi Martín
2024-03-29 20:01:45 +01:00
parent d3b1b21d3d
commit 35659d4419
15 changed files with 81 additions and 51 deletions

View File

@@ -30,7 +30,6 @@ gem "graphiql-rails", "~> 1.8.0"
gem "graphql", "~> 1.13.22" gem "graphql", "~> 1.13.22"
gem "groupdate", "~> 6.4.0" gem "groupdate", "~> 6.4.0"
gem "image_processing", "~> 1.12.2" gem "image_processing", "~> 1.12.2"
gem "initialjs-rails", "~> 0.2.0.9"
gem "invisible_captcha", "~> 2.3.0" gem "invisible_captcha", "~> 2.3.0"
gem "kaminari", "~> 1.2.2" gem "kaminari", "~> 1.2.2"
gem "mini_magick", "~> 4.12.0" gem "mini_magick", "~> 4.12.0"

View File

@@ -280,8 +280,6 @@ GEM
image_processing (1.12.2) image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
initialjs-rails (0.2.0.9)
railties (>= 3.1, < 7.0)
invisible_captcha (2.3.0) invisible_captcha (2.3.0)
rails (>= 5.2) rails (>= 5.2)
json (2.7.1) json (2.7.1)
@@ -719,7 +717,6 @@ DEPENDENCIES
groupdate (~> 6.4.0) groupdate (~> 6.4.0)
i18n-tasks (~> 0.9.37) i18n-tasks (~> 0.9.37)
image_processing (~> 1.12.2) image_processing (~> 1.12.2)
initialjs-rails (~> 0.2.0.9)
invisible_captcha (~> 2.3.0) invisible_captcha (~> 2.3.0)
kaminari (~> 1.2.2) kaminari (~> 1.2.2)
knapsack_pro (~> 7.0.1) knapsack_pro (~> 7.0.1)

View File

@@ -58,7 +58,6 @@
//= require ckeditor/loader //= require ckeditor/loader
//= require_directory ./ckeditor //= require_directory ./ckeditor
//= require social-share-button //= require social-share-button
//= require initial
//= require ahoy //= require ahoy
//= require app //= require app
//= require check_all_none //= require check_all_none
@@ -75,7 +74,6 @@
//= require annotator //= require annotator
//= require jquery.amsify.suggestags //= require jquery.amsify.suggestags
//= require tags //= require tags
//= require users
//= require participation_not_allowed //= require participation_not_allowed
//= require advanced_search //= require advanced_search
//= require registration_form //= require registration_form
@@ -136,7 +134,6 @@ var initialize_modules = function() {
App.Answers.initialize(); App.Answers.initialize();
App.Questions.initialize(); App.Questions.initialize();
App.Comments.initialize(); App.Comments.initialize();
App.Users.initialize();
App.ParticipationNotAllowed.initialize(); App.ParticipationNotAllowed.initialize();
App.Tags.initialize(); App.Tags.initialize();
App.FoundationExtras.initialize(); App.FoundationExtras.initialize();

View File

@@ -1,15 +0,0 @@
(function() {
"use strict";
App.Users = {
initialize: function() {
var observer;
$(".initialjs-avatar").initial();
observer = new MutationObserver(function(mutations) {
$.each(mutations, function(index, mutation) {
$(mutation.addedNodes).find(".initialjs-avatar").initial();
});
});
observer.observe(document.body, { childList: true, subtree: true });
}
};
}).call(this);

View File

@@ -25,6 +25,7 @@
@import "advanced_search"; @import "advanced_search";
@import "annotator_overrides"; @import "annotator_overrides";
@import "autocomplete_overrides"; @import "autocomplete_overrides";
@import "avatar";
@import "banner"; @import "banner";
@import "comments_count"; @import "comments_count";
@import "datepicker_overrides"; @import "datepicker_overrides";

View File

@@ -0,0 +1,4 @@
.initialjs-avatar {
font-family: HelveticaNeue-Light, "Helvetica Neue Light", "Helvetica Neue",
Helvetica, Arial, "Lucida Grande", sans-serif;
}

View File

@@ -1558,7 +1558,8 @@ table {
.comment-body, .comment-body,
.notification-body { .notification-body {
img { img,
svg {
margin-right: calc(#{$line-height} / 2); margin-right: calc(#{$line-height} / 2);
} }

View File

@@ -1 +1 @@
<%= avatar_image(record, options) %> <%= avatar_image %>

View File

@@ -1,6 +1,5 @@
class Shared::AvatarComponent < ApplicationComponent class Shared::AvatarComponent < ApplicationComponent
attr_reader :record, :given_options attr_reader :record, :given_options
use_helpers :avatar_image
def initialize(record, **given_options) def initialize(record, **given_options)
@record = record @record = record
@@ -10,7 +9,7 @@ class Shared::AvatarComponent < ApplicationComponent
private private
def default_options def default_options
{ background_color: colors[seed % colors.size], alt: "" } { background_color: colors[seed % colors.size], size: 100, color: "white" }
end end
def options def options
@@ -27,4 +26,53 @@ class Shared::AvatarComponent < ApplicationComponent
def seed def seed
record.id record.id
end end
def size
options[:size]
end
def font_size
(size * 0.6).round
end
def background_color
options[:background_color]
end
def color
options[:color]
end
def svg_options
{
xmlns: "http://www.w3.org/2000/svg",
width: size,
height: size,
role: "img",
"aria-label": "",
style: "background-color: #{background_color}",
class: "initialjs-avatar #{options[:class]}".strip
}
end
def text_options
{
x: "50%",
y: "50%",
dy: "0.35em",
"text-anchor": "middle",
fill: color,
style: "font-size: #{font_size}px"
}
end
def initial
record.name.first.upcase
end
def avatar_image
tag.svg(**svg_options) do
tag.text(initial, **text_options)
end
end
end end

View File

@@ -1,10 +1,22 @@
require "rails_helper" require "rails_helper"
describe Shared::AvatarComponent do describe Shared::AvatarComponent do
it "does not contain redundant text already present around it" do let(:user) { double(id: 1, name: "Johnny") }
render_inline Shared::AvatarComponent.new(double(id: 1, name: "Johnny")) let(:component) { Shared::AvatarComponent.new(user) }
expect(page).to have_css "img", count: 1 it "does not contain redundant text already present around it" do
expect(page).to have_css "img[alt='']" render_inline component
expect(page).to have_css "svg", count: 1
expect(page).to have_css "svg[role='img'][aria-label='']"
end
it "shows the initial letter of the name" do
render_inline component
page.find("svg") do |avatar|
expect(avatar).to have_text "J"
expect(avatar).not_to have_text "Johnny"
end
end end
end end

View File

@@ -1,9 +1,9 @@
RSpec::Matchers.define :have_avatar do |name, **options| RSpec::Matchers.define :have_avatar do |text, **options|
match do match do
has_css?("img.initialjs-avatar[data-name='#{name}'][src^='data:image/svg']", **options) has_css?("svg.initialjs-avatar", **{ exact_text: text }.merge(options))
end end
failure_message do failure_message do
"expected to find avatar with name #{name} but there were no matches." "expected to find avatar with text #{text} but there were no matches."
end end
end end

View File

@@ -15,7 +15,7 @@ describe "Account" do
expect(page).to have_current_path(account_path, ignore_query: true) expect(page).to have_current_path(account_path, ignore_query: true)
expect(page).to have_css "input[value='Manuela Colau']" expect(page).to have_css "input[value='Manuela Colau']"
expect(page).to have_avatar "Manuela Colau", count: 1 expect(page).to have_avatar "M", count: 1
end end
scenario "Show organization" do scenario "Show organization" do
@@ -26,7 +26,7 @@ describe "Account" do
expect(page).to have_css "input[value='Manuela Corp']" expect(page).to have_css "input[value='Manuela Corp']"
expect(page).not_to have_css "input[value='Manuela Colau']" expect(page).not_to have_css "input[value='Manuela Colau']"
expect(page).to have_avatar "Manuela Corp", count: 1 expect(page).to have_avatar "M", count: 1
end end
scenario "Edit" do scenario "Edit" do

View File

@@ -80,7 +80,7 @@ describe "Debates" do
expect(page).to have_content "Debate description" expect(page).to have_content "Debate description"
expect(page).to have_content "Charles Dickens" expect(page).to have_content "Charles Dickens"
expect(page).to have_content I18n.l(debate.created_at.to_date) expect(page).to have_content I18n.l(debate.created_at.to_date)
expect(page).to have_avatar "Charles Dickens" expect(page).to have_avatar "C"
expect(page.html).to include "<title>#{debate.title}</title>" expect(page.html).to include "<title>#{debate.title}</title>"
end end

View File

@@ -123,7 +123,7 @@ describe "Proposals" do
expect(page).to have_content "Proposal description" expect(page).to have_content "Proposal description"
expect(page).to have_content "Mark Twain" expect(page).to have_content "Mark Twain"
expect(page).to have_content I18n.l(proposal.created_at.to_date) expect(page).to have_content I18n.l(proposal.created_at.to_date)
expect(page).to have_avatar "Mark Twain" expect(page).to have_avatar "M"
expect(page.html).to include "<title>#{proposal.title}</title>" expect(page.html).to include "<title>#{proposal.title}</title>"
expect(page).not_to have_css ".js-flag-actions" expect(page).not_to have_css ".js-flag-actions"
expect(page).not_to have_css ".js-follow" expect(page).not_to have_css ".js-follow"

View File

@@ -485,18 +485,4 @@ describe "Users" do
expect(page).not_to have_content("Sport") expect(page).not_to have_content("Sport")
end end
end end
describe "Initials" do
scenario "display SVG avatars when loaded into the DOM" do
login_as(create(:user, username: "Commentator"))
visit debate_path(create(:debate))
fill_in "Leave your comment", with: "I'm awesome"
click_button "Publish comment"
within ".comment", text: "I'm awesome" do
expect(page).to have_avatar "Commentator"
end
end
end
end end