Allow admins to set brand color (#10097)

* Allow admins to set brand color

* Remove extra line

* Fix linting

* Update app/controllers/admin/configs_controller.rb

Co-authored-by: Michael Kohl <citizen428@dev.to>

* Move wcag compare to own class

* Remove unnecessary spacing

Co-authored-by: Michael Kohl <citizen428@dev.to>
This commit is contained in:
Ben Halpern 2020-09-01 05:16:31 -04:00 committed by GitHub
parent cc7d297371
commit 7b5bc89b7c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 124 additions and 2 deletions

View file

@ -108,6 +108,7 @@ gem "uglifier", "~> 4.2" # Uglifier minifies JavaScript files
gem "ulid", "~> 1.2" # Universally Unique Lexicographically Sortable Identifier implementation for Ruby
gem "validate_url", "~> 1.0" # Library for validating urls in Rails
gem "vault", "~> 0.15" # Used to store secrets
gem "wcag_color_contrast", "~> 0.1" # Detect contrast of colors to determine readability and a11y.
gem "webpacker", "~> 5.2.1" # Use webpack to manage app-like JavaScript modules in Rails
group :development do

View file

@ -815,6 +815,7 @@ GEM
equalizer (~> 0.0, >= 0.0.9)
warden (1.2.8)
rack (>= 2.0.6)
wcag_color_contrast (0.1.0)
web-console (4.0.4)
actionview (>= 6.0.0)
activemodel (>= 6.0.0)
@ -995,6 +996,7 @@ DEPENDENCIES
validate_url (~> 1.0)
vault (~> 0.15)
vcr (~> 6.0)
wcag_color_contrast (~> 0.1)
web-console (~> 4.0)
webdrivers (~> 4.4)
webmock (~> 3.8)

View file

@ -12,6 +12,10 @@
--brand-github-bg: #24292e;
--brand-github-color: #fff;
--brand-github-bg-hover: #000;
--brand-facebook-bg: #4267B2;
--brand-facebook-color: #fff;
--brand-facebook-bg-hover: #476fbf;
}
// Basic styling
@ -261,6 +265,18 @@
--color-inverted-hover: var(--brand-github-color);
}
.crayons-btn--brand-facebook {
--bg: var(--brand-facebook-bg);
--bg-hover: var(--brand-facebook-bg-hover);
--color: var(--brand-facebook-color);
--color-hover: var(--brand-facebook-color);
--bg-inverted: var(--brand-facebook-bg);
--bg-inverted-hover: var(--brand-facebook-bg-hover);
--color-inverted: var(--brand-facebook-color);
--color-inverted-hover: var(--brand-facebook-color);
}
// Icon alone
.crayons-btn--icon,
.crayons-btn--icon-rounded {

View file

@ -3,6 +3,7 @@ module Admin
layout "admin"
before_action :extra_authorization_and_confirmation, only: [:create]
before_action :validate_inputs, only: [:create]
def show
@confirmation_text = confirmation_text
@ -54,6 +55,7 @@ module Admin
facebook_key
facebook_secret
allow_email_password_registration
primary_brand_color_hex
]
allowed_params = allowed_params |
@ -87,6 +89,13 @@ module Admin
raise_confirmation_mismatch_error if params.require(:confirmation) != confirmation_text
end
def validate_inputs
errors = []
errors << "Brand color must be darker for accessibility." if brand_contrast_too_low
errors << "Brand color must be be a 6 character hex (starting with #)." if brand_color_not_hex
redirect_to admin_config_path, alert: "😭 #{errors.join(',')}" if errors.any?
end
def clean_up_params
config = params[:site_config]
%i[sidebar_tags suggested_tags suggested_users].each do |param|
@ -103,6 +112,17 @@ module Admin
Rails.cache.delete_matched(ApplicationConfig["RELEASE_FOOTPRINT"]) # Delete all caches tied to this key.
end
# Validations
def brand_contrast_too_low
hex = params[:site_config][:primary_brand_color_hex]
hex.present? && Color::Accessibility.new(hex).low_contrast?
end
def brand_color_not_hex
hex = params[:site_config][:primary_brand_color_hex]
hex.present? && !hex.match?(/\A#(\h{6}|\h{3})\z/)
end
def campaign_params
%i[
campaign_featured_tags

View file

@ -101,6 +101,10 @@ module Constants
description: "Determines which default feed the users sees (rich content, more minimal, etc.)",
placeholder: "basic, rich, or compact"
},
primary_brand_color_hex: {
description: "Determines background/border of buttons etc. Must be dark enough to contrast with white text.",
placeholder: "#0a0a0a"
},
github_key: {
description: "The \"Client ID\" portion of the GitHub Oauth Apps portal",
placeholder: ""

View file

@ -153,6 +153,7 @@ class SiteConfig < RailsSettings::Base
field :public, type: :boolean, default: 0
# The default font for all users that have not chosen a custom font yet
field :default_font, type: :string, default: "sans_serif"
field :primary_brand_color_hex, type: :string, default: "#3b49df"
# Broadcast
field :welcome_notifications_live_at, type: :date

View file

@ -0,0 +1,11 @@
module Color
class Accessibility
def initialize(hex)
@hex = hex.delete("#")
end
def low_contrast?(compared_color = "ffffff", min_contrast = 4.5)
WCAGColorContrast.ratio(@hex, compared_color.delete("#")) < min_contrast
end
end
end

View file

@ -931,7 +931,7 @@
<div class="card mt-3">
<%= render partial: "card_header",
locals: {
header: "User Experience",
header: "User Experience and Brand",
state: "collapse",
target: "uxBodyContainer",
expanded: "false"
@ -956,6 +956,15 @@
class: "form-control selectpicker" %>
<div class="alert alert-info"><%= Constants::SiteConfig::DETAILS[:default_font][:description] %></div>
</div>
<div class="form-group">
<%= f.label :primary_brand_color_hex %>
<%= f.text_field :primary_brand_color_hex,
class: "form-control",
value: SiteConfig.primary_brand_color_hex,
pattern: "^#+([a-fA-F0-9]{6})$",
placeholder: Constants::SiteConfig::DETAILS[:primary_brand_color_hex][:placeholder] %>
<div class="alert alert-info"><%= Constants::SiteConfig::DETAILS[:primary_brand_color_hex][:description] %></div>
</div>
<div class="form-group">
<%= admin_config_label :public %>
<%= f.check_box :public, checked: SiteConfig.public %>

View file

@ -55,7 +55,16 @@
class="<%= SiteConfig.default_font.tr("_", "-") %>-article-body"
data-pusher-key="<%= ApplicationConfig["PUSHER_KEY"] %>"
data-app-name="<%= ApplicationConfig["APP_NAME"] %>">
<div id="body-styles"></div>
<div id="body-styles">
<style>
:root {
--accent-brand: <%= SiteConfig.primary_brand_color_hex %>;
--accent-brand-darker: <%= HexComparer.new([SiteConfig.primary_brand_color_hex]).brightness(0.85) %>;
--accent-brand-lighter: <%= HexComparer.new([SiteConfig.primary_brand_color_hex]).brightness(1.1) %>;
--accent-brand-a10: #{rgba(<%= SiteConfig.primary_brand_color_hex %>, 0.1)};
}
</style>
</div>
<% if user_signed_in? %>
<%= render "layouts/user_config" %>
<% end %>

View file

@ -589,6 +589,28 @@ RSpec.describe "/admin/config", type: :request do
expect(SiteConfig.feed_style).to eq(feed_style)
end
it "updates the brand color if proper hex" do
hex = "#0a0a0a" # dark enough
post "/admin/config", params: { site_config: { primary_brand_color_hex: hex },
confirmation: confirmation_message }
expect(SiteConfig.primary_brand_color_hex).to eq(hex)
end
it "does not update brand color if hex not contrasting enough" do
hex = "#bd746f" # not dark enough
post "/admin/config", params: { site_config: { primary_brand_color_hex: hex },
confirmation: confirmation_message }
expect(SiteConfig.primary_brand_color_hex).not_to eq(hex)
end
it "does not update brand color if hex not a hex with proper format" do
hex = "0a0a0a" # dark enough, but not proper format
post "/admin/config", params: { site_config: { primary_brand_color_hex: hex },
confirmation: confirmation_message }
expect(SiteConfig.primary_brand_color_hex).not_to eq(hex)
end
it "updates public to true" do
is_public = true
post "/admin/config", params: { site_config: { public: is_public },

View file

@ -0,0 +1,27 @@
require "rails_helper"
RSpec.describe Color::Accessibility, type: :service do
it "determines low contrast with default compared color" do
expect(described_class.new("#8a9bb8").low_contrast?).to be true
end
it "determines low contrast with compared color input" do
expect(described_class.new("#041d4a").low_contrast?("#1a3669")).to be true
end
it "determines low contrast with compared color and rate" do
expect(described_class.new("#225CC9").low_contrast?("#ffffff", 10.0)).to be true
end
it "determines sufficient contrast with default compared color" do
expect(described_class.new("#041d4a").low_contrast?).to be false
end
it "determines sufficient contrast with compared color input" do
expect(described_class.new("#041d4a").low_contrast?("#e6eaf0")).to be false
end
it "determines sufficient contrast with compared color and rate" do
expect(described_class.new("#225CC9").low_contrast?("#ffffff", 1.0)).to be false
end
end

Binary file not shown.