Allow users to dismiss announcements (#8396) [deploy]

* Provide default crayon styles when creating banners

* Import crayons styles into /internal pages.
* Allow admins to use crayons banner styles when creating broadcasts.
* Preview banner styles with crayons.

* Add a "x" close button to announcements, fix styling to accommodate it

* Use constant and helper for broadcast banner styles

* Add VALID_BANNER_STYLES frozen constant to Broadcast class.
* Add banner_style helper for determining banner style class.

* Clean up preview CSS, import broadcast styles into preview

* Add close button click handler, clean up initializeBroadcast

* Add close functionality to announcement banner

Also hide announcement if the user has explicitly seen and "X"-ed out of it.

* Add system specs around rendering + dismissing broadcasts

* Add some (truly beautiful) JS documentation

* Add visible class to broadcast when previewed from internal
This commit is contained in:
Vaidehi Joshi 2020-06-11 08:24:43 -07:00 committed by GitHub
parent 2b30f769f3
commit e532ccbe42
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 202 additions and 34 deletions

View file

@ -3,6 +3,7 @@
* Parses the broadcast object on the document into JSON.
*
* @function broadcastData
* @returns {Object} Returns an object of the parsed broadcast data.
*/
function broadcastData() {
const { broadcast = null } = document.body.dataset;
@ -11,8 +12,77 @@ function broadcastData() {
}
/**
* Inserts the broadcast's HTML into `active-broadcast` element
* as the first child within the document's body, and only inserts the HTML once.
* Parses the broadcast object on the document into JSON.
*
* @function camelizedBroadcastKey
* @param {string} title The title of the broadcast.
* @returns {string} Returns the camelized title appended with "Seen".
*/
function camelizedBroadcastKey(title) {
const camelizedTitle = title.replace(/\W+(.)/g, (match, string) => {
return string.toUpperCase();
});
return `${camelizedTitle}Seen`;
}
/**
* A function that finds the close button and adds a click handler to it.
*
* @function addCloseButtonClickHandle
* @param {string} title The title of the broadcast.
*/
function addCloseButtonClickHandle(title) {
var closeButton = document.getElementsByClassName(
'close-announcement-button',
)[0];
closeButton.onclick = (e) => {
document.getElementById('active-broadcast').style.display = 'none';
localStorage.setItem(camelizedBroadcastKey(title), true);
};
}
/**
* A function to insert the broadcast's HTML into the `active-broadcast` element.
* Determines what classes to add to the broadcast element,
* and inserts a close button and adds a click handler to it.
*
* Adds a `.visible` class to the broadcastElement to make it render.
*
* @function initializeBroadcast
* @param {string} broadcastElement The HTML element for the broadcast, with a class of `.active-broadcast`.
* @param {Object} data An object representing the parsed broadcast data.
*/
function renderBroadcast(broadcastElement, data) {
const { banner_class, html, title } = data;
if (banner_class) {
const [defaultClass, additionalClass] = banner_class.split(' ');
if (additionalClass) {
broadcastElement.classList.add(defaultClass, additionalClass);
} else {
broadcastElement.classList.add(defaultClass);
}
}
const closeButton = `<button class="close-announcement-button">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.99974 5.58623L11.9497 0.63623L13.3637 2.05023L8.41374 7.00023L13.3637 11.9502L11.9497 13.3642L6.99974 8.41423L2.04974 13.3642L0.635742 11.9502L5.58574 7.00023L0.635742 2.05023L2.04974 0.63623L6.99974 5.58623Z" fill="white" />
</svg>
</button>`;
broadcastElement.insertAdjacentHTML(
'afterbegin',
`<div class='broadcast-data'>${html}</div>${closeButton}`,
);
addCloseButtonClickHandle(title);
broadcastElement.classList.add('visible');
}
/**
* A function to determine if a broadcast should render
* Does not render broadcast it has already been inserted,
* or if the key for the broadcast's title exists in localStorage.
*
* @function initializeBroadcast
*/
@ -21,16 +91,17 @@ function initializeBroadcast() {
if (!data) {
return;
}
const { html, banner_class } = data;
const el = document.getElementById('active-broadcast');
const { title } = data;
if (JSON.parse(localStorage.getItem(camelizedBroadcastKey(title))) === true) {
return; // Do not render broadcast if previously dismissed by user.
}
const el = document.getElementById('active-broadcast');
if (el.firstElementChild) {
return; // Only append HTML once, on first render.
}
const bannerDiv = `<div class='broadcast-data ${
banner_class || ''
}'>${html}</div>`;
el.insertAdjacentHTML('afterbegin', bannerDiv);
renderBroadcast(el, data);
}
/* eslint-enable camelcase */

View file

@ -1,5 +1,6 @@
@import '../variables';
@import '../crayons';
@import '../scaffolds';
@import '../variables';
#siteConfigBodyContainer {
padding: 20px;
@ -50,6 +51,11 @@
border: 20px solid black;
}
.broadcast-wrapper {
position: relative;
z-index: auto;
}
.sidebar-nav {
min-height: 100vh;
background: $light-gray;

View file

@ -165,13 +165,31 @@ body.ten-x-hacker-theme {
}
.broadcast-wrapper {
display: none;
width: 100%;
font-size: 20px;
text-align: center;
padding: 0;
min-height: 45px;
// TODO: [@thepracticaldev/delightful]: Remove these styles once
// https://github.com/thepracticaldev/dev.to/pull/8234 is merged.
z-index: 101; // Must be higher than the z-index of the sticky-nav.
position: fixed;
width: 100%;
font-size: 20px;
text-align: center;
&.visible {
display: flex;
}
}
.broadcast-data {
flex-grow: 1;
padding: var(--su-2);
}
.close-announcement-button {
background-color: transparent;
border: none;
}
body.static-navbar-config {

View file

@ -22,8 +22,10 @@
<% if @broadcast.processed_html %>
<div>
</p><strong>Preview</strong></p>
<div class="<%= banner_class(@broadcast) %>">
<%= sanitize @broadcast.processed_html, attributes: %w[href style src] %>
<div class="broadcast-wrapper visible <%= banner_class(@broadcast) %>">
<div class="broadcast-data">
<%= sanitize @broadcast.processed_html, attributes: %w[href style src] %>
</div>
</div>
</p><em>Please note: announcement broadcasts will render directly below the nav bar once activated.</em></p>
<div>

View file

@ -65,7 +65,7 @@ FactoryBot.define do
factory :announcement_broadcast do
title { "A Very Important Announcement" }
type_of { "Announcement" }
processed_html { "<div style='background-color: salmon;'><p style='width: 100%;'>Hello, World!</p></div>" }
processed_html { "<p>Hello, World!</p>" }
end
end
end

View file

@ -31,25 +31,6 @@ RSpec.describe "User visits a homepage", type: :system do
expect(page).to have_text("DESIGN YOUR EXPERIENCE")
end
context "when rendering broadcasts" do
let!(:broadcast) { create(:announcement_broadcast) }
it "renders the broadcast if active", js: true do
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
visit "/"
within ".broadcast-wrapper" do
expect(page).to have_text("Hello, World!")
end
end
it "does not render a broadcast if inactive", js: true do
broadcast.update!(active: false)
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
visit "/"
expect(page).not_to have_css(".broadcast-wrapper")
end
end
describe "link tags" do
it "contains the qualified community name in the search link" do
visit "/"

View file

@ -0,0 +1,90 @@
require "rails_helper"
def expect_broadcast_data(page)
within ".broadcast-wrapper" do
expect(page).to have_selector(".broadcast-data")
expect(page).to have_text("Hello, World!")
end
end
def expect_no_broadcast_data(page)
expect(page).not_to have_css(".broadcast-wrapper")
expect(page).not_to have_selector(".broadcast-data")
expect(page).not_to have_text("Hello, World!")
end
RSpec.describe "User visits a homepage", type: :system do
context "when user hasn't logged in" do
context "with an active announcement" do
before do
create(:announcement_broadcast)
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
visit "/"
end
it "renders the broadcast", js: true do
expect_broadcast_data(page)
end
it "dismisses the broadcast", js: true do
wait_for_javascript
find(".close-announcement-button").click
expect_no_broadcast_data(page)
end
end
context "without an active announcement" do
before do
create(:announcement_broadcast, active: false)
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
visit "/"
end
it "does not render the broadcast", js: true do
expect_no_broadcast_data(page)
end
end
end
context "when user has logged in" do
let(:user) { create(:user) }
before do
sign_in(user)
end
context "with an active announcement" do
before do
create(:announcement_broadcast)
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
visit "/"
end
it "renders the broadcast", js: true do
expect_broadcast_data(page)
end
it "dismisses the broadcast", js: true do
get "/async_info/base_data"
visit "/"
wait_for_javascript
find(".close-announcement-button").click
expect_no_broadcast_data(page)
end
end
context "without an active announcement" do
before do
create(:announcement_broadcast, active: false)
get "/async_info/base_data" # Explicitly ensure broadcast data is loaded before doing any checks
visit "/"
end
it "does not render the broadcast", js: true do
expect_no_broadcast_data(page)
end
end
end
end