diff --git a/Gemfile b/Gemfile index 50715b048..046c6f72c 100644 --- a/Gemfile +++ b/Gemfile @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index f4c5ee119..a2f6da871 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/assets/stylesheets/components/buttons.scss b/app/assets/stylesheets/components/buttons.scss index e1a2d1699..37f585369 100644 --- a/app/assets/stylesheets/components/buttons.scss +++ b/app/assets/stylesheets/components/buttons.scss @@ -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 { diff --git a/app/controllers/admin/configs_controller.rb b/app/controllers/admin/configs_controller.rb index dd9d5fb53..0d8d11a47 100644 --- a/app/controllers/admin/configs_controller.rb +++ b/app/controllers/admin/configs_controller.rb @@ -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 diff --git a/app/lib/constants/site_config.rb b/app/lib/constants/site_config.rb index a44af0be1..c4448ebef 100644 --- a/app/lib/constants/site_config.rb +++ b/app/lib/constants/site_config.rb @@ -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: "" diff --git a/app/models/site_config.rb b/app/models/site_config.rb index 764627dd1..dafee7215 100644 --- a/app/models/site_config.rb +++ b/app/models/site_config.rb @@ -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 diff --git a/app/services/color/accessibility.rb b/app/services/color/accessibility.rb new file mode 100644 index 000000000..a2c381e77 --- /dev/null +++ b/app/services/color/accessibility.rb @@ -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 diff --git a/app/views/admin/configs/show.html.erb b/app/views/admin/configs/show.html.erb index 05f1afd09..ba24a205b 100644 --- a/app/views/admin/configs/show.html.erb +++ b/app/views/admin/configs/show.html.erb @@ -931,7 +931,7 @@
<%= 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" %>
<%= Constants::SiteConfig::DETAILS[:default_font][:description] %>
+
+ <%= 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] %> +
<%= Constants::SiteConfig::DETAILS[:primary_brand_color_hex][:description] %>
+
<%= admin_config_label :public %> <%= f.check_box :public, checked: SiteConfig.public %> diff --git a/app/views/shell/_top.html.erb b/app/views/shell/_top.html.erb index b00a45225..502ba964c 100644 --- a/app/views/shell/_top.html.erb +++ b/app/views/shell/_top.html.erb @@ -55,7 +55,16 @@ class="<%= SiteConfig.default_font.tr("_", "-") %>-article-body" data-pusher-key="<%= ApplicationConfig["PUSHER_KEY"] %>" data-app-name="<%= ApplicationConfig["APP_NAME"] %>"> -
+
+ +
<% if user_signed_in? %> <%= render "layouts/user_config" %> <% end %> diff --git a/spec/requests/admin/configs_spec.rb b/spec/requests/admin/configs_spec.rb index 46590355d..56a16b706 100644 --- a/spec/requests/admin/configs_spec.rb +++ b/spec/requests/admin/configs_spec.rb @@ -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 }, diff --git a/spec/services/color/accessibility_spec.rb b/spec/services/color/accessibility_spec.rb new file mode 100644 index 000000000..cb26f95b1 --- /dev/null +++ b/spec/services/color/accessibility_spec.rb @@ -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 diff --git a/vendor/cache/wcag_color_contrast-0.1.0.gem b/vendor/cache/wcag_color_contrast-0.1.0.gem new file mode 100644 index 000000000..f0417e2e3 Binary files /dev/null and b/vendor/cache/wcag_color_contrast-0.1.0.gem differ