diff --git a/CHANGELOG.md b/CHANGELOG.md index 92993ebe..1b7dab7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +- ### Fixes accessibility issues with Article and Article List + - Adds alt text to Article List images. + - Enhances readability and fixes bugs with article title backgrounds. + + Resolves CuBoulder/tiamat-theme#616 +--- + - ### Fixes expandable content `aria-expanded` errors Resolves CuBoulder/tiamat-theme#614 --- diff --git a/css/ucb-article.css b/css/ucb-article.css index 56b45410..fba7ed70 100644 --- a/css/ucb-article.css +++ b/css/ucb-article.css @@ -131,7 +131,7 @@ i.fa-regular.fa-calendar { margin-bottom: 20px; } -.ucb-article-author-name span:not(:last-of-type):after{ +.ucb-article-author-name span:not(:last-of-type):after { content: ","; } @@ -142,50 +142,58 @@ i.fa-regular.fa-calendar { object-fit: fill; } -.centeredTitle { +.backgroundTitleDiv .ucb-article-heading { position: absolute; - bottom: 0.5em; + display: block; + bottom: 0; + width: 100%; + background-color: transparent; + padding: 200px 20px 30px; +} + +.backgroundTitleDiv.ucb-overlay-dark .ucb-article-heading { + background-image: -webkit-linear-gradient(top, transparent, rgba(0, 0, 0, 0.85)); + background-image: -moz-linear-gradient(top, transparent, rgba(0, 0, 0, 0.85)); + background-image: -o-linear-gradient(top, transparent, rgba(0, 0, 0, 0.85)); + background-image: -ms-linear-gradient(top, transparent, rgba(0, 0, 0, 0.85)); + background-image: linear-gradient(top, transparent, rgba(0, 0, 0, 0.85)); +} + +.backgroundTitleDiv.ucb-overlay-light .ucb-article-heading { + background-image: -webkit-linear-gradient(top, transparent, rgba(255, 255, 255, 0.85)); + background-image: -moz-linear-gradient(top, transparent, rgba(255, 255, 255, 0.85)); + background-image: -o-linear-gradient(top, transparent, rgba(255, 255, 255, 0.85)); + background-image: -ms-linear-gradient(top, transparent, rgba(255, 255, 255, 0.85)); + background-image: linear-gradient(top, transparent, rgba(255, 255, 255, 0.85)); } -.article-header-White{ - color: white; +.ucb-article-heading.article-header-White, .ucb-article-heading.article-header-White h1 { + color: #FFF; } -.article-header-Black{ - color:black +.ucb-article-heading.article-header-Black, .ucb-article-heading.article-header-Black h1 { + color: #000; } -.backgroundTitleDiv > div > div > .imageMediaStyle > img { +.backgroundTitleDiv .imageMediaStyle img { width: 100%; max-height: 600px; object-fit: cover; } -.backgroundTitleDiv > div > div > .imageMediaStyle{ +.backgroundTitleDiv .imageMediaStyle { padding-bottom: 0; margin-bottom: 20px; } -.ucb-overlay-dark > div > div > .imageMediaStyle > img, -.ucb-overlay-light > div > div > .imageMediaStyle > img{ - opacity: 75%; -} - -.ucb-overlay-light > div > div > .imageMediaStyle{ - background: white; -} -.ucb-overlay-dark > div > div > .imageMediaStyle{ - background: black; -} - -.ucb-article-supplemental-text{ +.ucb-article-supplemental-text { margin-bottom: 1em; font-style: italic; font-size: 0.85em; } /* Removes caption on header img */ -.backgroundTitleDiv > div > div > .media-image-caption{ +.backgroundTitleDiv .media-image-caption { display: none; } @@ -194,6 +202,6 @@ i.fa-regular.fa-calendar { display: block; } -.backgroundTitleDiv .imageMediaStyle img{ +.backgroundTitleDiv .imageMediaStyle img { padding-bottom: 0px; } diff --git a/js/ucb-article-list.js b/js/ucb-article-list.js index 558c9282..5a98e4ba 100644 --- a/js/ucb-article-list.js +++ b/js/ucb-article-list.js @@ -1,15 +1,15 @@ /** * Get additional data from the paragraph content attached to the Article node * @param {string} id - internal id used by Drupal to get the specific paragraph - */ + */ async function getArticleParagraph(id) { - if(id) { + if (id) { const response = await fetch( `/jsonapi/paragraph/article_content/${id}` ); return response; } else { - return ""; + return ""; } } @@ -49,173 +49,175 @@ function toggleMessage(id, display = "none") { * @param {string} ExcludeTags - array of tags to filter out when rendering * @returns - Promise with resolve or reject */ -function renderArticleList( JSONURL, ExcludeCategories = "", ExcludeTags = "") { - return new Promise(function(resolve, reject) { - let excludeCatArray = ExcludeCategories.split(",").map(Number); - let excludeTagArray = ExcludeTags.split(",").map(Number); - // next URL if there is one, will be returned by this funtion - let NEXTJSONURL = ""; - - if (JSONURL) { - //let el = document.getElementById(id); - - // show the loading spinner while we load the data - toggleMessage("ucb-al-loading", "block"); - - fetch(JSONURL) - .then((reponse) => reponse.json()) - .then((data) => { - // get the next URL and return that if there is one - if(data.links.next) { - let nextURL = data.links.next.href.split("/jsonapi/"); - NEXTJSONURL = nextURL[1]; - } else { - NEXTJSONURL = ""; - } +function renderArticleList(JSONURL, ExcludeCategories = "", ExcludeTags = "") { + return new Promise(function (resolve, reject) { + let excludeCatArray = ExcludeCategories.split(",").map(Number); + let excludeTagArray = ExcludeTags.split(",").map(Number); + // next URL if there is one, will be returned by this funtion + let NEXTJSONURL = ""; + + if (JSONURL) { + //let el = document.getElementById(id); + + // show the loading spinner while we load the data + toggleMessage("ucb-al-loading", "block"); + + fetch(JSONURL) + .then((reponse) => reponse.json()) + .then((data) => { + // get the next URL and return that if there is one + if (data.links.next) { + let nextURL = data.links.next.href.split("/jsonapi/"); + NEXTJSONURL = nextURL[1]; + } else { + NEXTJSONURL = ""; + } - //console.log("data obj", data); + //console.log("data obj", data); - // if no articles of returned, stop the loading spinner and let the user know we received no data that matches their query - if (!data.data.length) { - toggleMessage("ucb-al-loading", "none"); - toggleMessage("ucb-al-no-results", "block"); - reject; - } + // if no articles of returned, stop the loading spinner and let the user know we received no data that matches their query + if (!data.data.length) { + toggleMessage("ucb-al-loading", "none"); + toggleMessage("ucb-al-no-results", "block"); + reject; + } - // Below objects are needed to match images with their corresponding articles. There are two endpoints => data.data (article) and data.included (incl. media), both needed to associate a media library image with its respective article - let urlObj = {}; - let idObj = {}; - let altObj = {}; - // Remove any blanks from our articles before map - if (data.included) { - // removes all other included data besides images in our included media - let idFilterData = data.included.filter((item) => { - return item.type == "media--image"; - }) + // Below objects are needed to match images with their corresponding articles. There are two endpoints => data.data (article) and data.included (incl. media), both needed to associate a media library image with its respective article + let urlObj = {}; + let idObj = {}; + let altObj = {}; + // Remove any blanks from our articles before map + if (data.included) { + // removes all other included data besides images in our included media + let idFilterData = data.included.filter((item) => { + return item.type == "media--image"; + }) + + let altFilterData = data.included.filter((item) => { + return item.type == 'file--file'; + }); + // finds the focial point version of the thumbnail + altFilterData.map((item) => { + // checks if consumer is working, else default to standard image instead of focal image + if (item.links.focal_image_square != undefined) { + altObj[item.id] = { src: item.links.focal_image_square.href } + } else { + altObj[item.id] = { src: item.attributes.uri.url } + } + }) + + // using the image-only data, creates the idObj => key: thumbnail id, value : data id + idFilterData.map((pair) => { + const thumbnailId = pair.relationships.thumbnail.data.id; + idObj[pair.id] = pair.relationships.thumbnail.data.id; + altObj[thumbnailId].alt = pair.relationships.thumbnail.data.meta.alt; + }) + } + // console.log("idObj", idObj); + // console.log("urlObj", urlObj); + // console.log('altObj', altObj) + //iterate over each item in the array + data.data.map((item) => { + let thisArticleCats = []; + let thisArticleTags = []; + // // loop through and grab all of the categories + if (item.relationships.field_ucb_article_categories) { + for (let i = 0; i < item.relationships.field_ucb_article_categories.data.length; i++) { + thisArticleCats.push( + item.relationships.field_ucb_article_categories.data[i].meta + .drupal_internal__target_id + ) + } + } + // console.log("this article cats",thisArticleCats) - let altFilterData = data.included.filter((item) => { - return item.type == 'file--file'; - }); - // finds the focial point version of the thumbnail - altFilterData.map((item)=>{ - // checks if consumer is working, else default to standard image instead of focal image - if(item.links.focal_image_square != undefined){ - altObj[item.id] = item.links.focal_image_square.href - } else { - altObj[item.id] = item.attributes.uri.url + // // loop through and grab all of the tags + if (item.relationships.field_ucb_article_tags) { + for (var i = 0; i < item.relationships.field_ucb_article_tags.data.length; i++) { + thisArticleTags.push(item.relationships.field_ucb_article_tags.data[i].meta.drupal_internal__target_id) + } } - }) + // console.log('this article tags',thisArticleTags) - // using the image-only data, creates the idObj => key: thumbnail id, value : data id - idFilterData.map((pair) => { - idObj[pair.id] = pair.relationships.thumbnail.data.id; - }) - } - // console.log("idObj", idObj); - // console.log("urlObj", urlObj); - // console.log('altObj', altObj) - //iterate over each item in the array - data.data.map((item) => { - let thisArticleCats = []; - let thisArticleTags = []; - // // loop through and grab all of the categories - if (item.relationships.field_ucb_article_categories) { - for (let i = 0; i < item.relationships.field_ucb_article_categories.data.length; i++) { - thisArticleCats.push( - item.relationships.field_ucb_article_categories.data[i].meta - .drupal_internal__target_id + // checks to see if the current article (item) contains a category or tag scheduled for exclusion + let doesIncludeCat = thisArticleCats; + let doesIncludeTag = thisArticleTags; + + // check to see if we need to filter on categories + if (excludeCatArray.length && thisArticleCats.length) { + doesIncludeCat = thisArticleCats.filter((element) => + excludeCatArray.includes(element) ) } - } - // console.log("this article cats",thisArticleCats) - - // // loop through and grab all of the tags - if (item.relationships.field_ucb_article_tags) { - for (var i = 0; i < item.relationships.field_ucb_article_tags.data.length; i++) { - thisArticleTags.push(item.relationships.field_ucb_article_tags.data[i].meta.drupal_internal__target_id) + // check to see if we need to filter on tags + if (excludeTagArray.length && thisArticleTags.length) { + doesIncludeTag = thisArticleTags.filter((element) => + excludeTagArray.includes(element) + ) } - } - // console.log('this article tags',thisArticleTags) - // checks to see if the current article (item) contains a category or tag scheduled for exclusion - let doesIncludeCat = thisArticleCats; - let doesIncludeTag = thisArticleTags; - - // check to see if we need to filter on categories - if (excludeCatArray.length && thisArticleCats.length) { - doesIncludeCat = thisArticleCats.filter((element) => - excludeCatArray.includes(element) - ) - } - // check to see if we need to filter on tags - if (excludeTagArray.length && thisArticleTags.length) { - doesIncludeTag = thisArticleTags.filter((element) => - excludeTagArray.includes(element) - ) - } - - // if we didn't match any of the filtered tags or cats, then render the content - if (doesIncludeCat.length == 0 && doesIncludeTag.length == 0) { - // we need to render the Article Card view for this returned element - // **ADD DATA** - // this is my id of the article body paragraph type we need only if no thumbnail or summary provided - let bodyAndImageId = item.relationships.field_ucb_article_content.data.length ? item.relationships.field_ucb_article_content.data[0].id : ""; - let body = item.attributes.field_ucb_article_summary ? item.attributes.field_ucb_article_summary : ""; - body = body.trim(); - let imageSrc = ""; - - // if no article summary, use a simplified article body - if (!body.length && bodyAndImageId != "") { - getArticleParagraph(bodyAndImageId) - .then((response) => response.json()) - .then((data) => { - // Remove any html tags within the article - let htmlStrip = data.data.attributes.field_article_text.processed.replace( - /<\/?[^>]+(>|$)/g, - "" - ) - // Remove any line breaks if media is embedded in the body - let lineBreakStrip = htmlStrip.replace(/(\r\n|\n|\r)/gm, ""); - // take only the first 100 words ~ 500 chars - let trimmedString = lineBreakStrip.substr(0, 250); - // if in the middle of the string, take the whole word - if(trimmedString.length > 100){ - trimmedString = trimmedString.substr( - 0, - Math.min( - trimmedString.length, - trimmedString.lastIndexOf(" ") - ) + // if we didn't match any of the filtered tags or cats, then render the content + if (doesIncludeCat.length == 0 && doesIncludeTag.length == 0) { + // we need to render the Article Card view for this returned element + // **ADD DATA** + // this is my id of the article body paragraph type we need only if no thumbnail or summary provided + let bodyAndImageId = item.relationships.field_ucb_article_content.data.length ? item.relationships.field_ucb_article_content.data[0].id : ""; + let body = item.attributes.field_ucb_article_summary ? item.attributes.field_ucb_article_summary : ""; + body = body.trim(); + let imageSrc = ""; + + // if no article summary, use a simplified article body + if (!body.length && bodyAndImageId != "") { + getArticleParagraph(bodyAndImageId) + .then((response) => response.json()) + .then((data) => { + // Remove any html tags within the article + let htmlStrip = data.data.attributes.field_article_text.processed.replace( + /<\/?[^>]+(>|$)/g, + "" ) - body = `${trimmedString}...`; - } - // set the contentBody of Article Summary card to the minified body instead - body = `${trimmedString}`; - document.getElementById(`body-${bodyAndImageId}`).innerText = body; - }) - } + // Remove any line breaks if media is embedded in the body + let lineBreakStrip = htmlStrip.replace(/(\r\n|\n|\r)/gm, ""); + // take only the first 100 words ~ 500 chars + let trimmedString = lineBreakStrip.substr(0, 250); + // if in the middle of the string, take the whole word + if (trimmedString.length > 100) { + trimmedString = trimmedString.substr( + 0, + Math.min( + trimmedString.length, + trimmedString.lastIndexOf(" ") + ) + ) + body = `${trimmedString}...`; + } + // set the contentBody of Article Summary card to the minified body instead + body = `${trimmedString}`; + document.getElementById(`body-${bodyAndImageId}`).innerText = body; + }) + } - // if no thumbnail, show no image - if (!item.relationships.field_ucb_article_thumbnail.data) { - imageSrc = ""; - } else { - //Use the idObj as a memo to add the corresponding image url - let thumbId = item.relationships.field_ucb_article_thumbnail.data.id; - imageSrc = altObj[idObj[thumbId]]; - } + // if no thumbnail, show no image + if (!item.relationships.field_ucb_article_thumbnail.data) { + imageSrc = null; + } else { + //Use the idObj as a memo to add the corresponding image url + let thumbId = item.relationships.field_ucb_article_thumbnail.data.id; + imageSrc = altObj[idObj[thumbId]]; + } - //Date - make human readable - const options = { year: 'numeric', month: 'short', day: 'numeric' }; - let date = new Date(item.attributes.created).toLocaleDateString('en-us', options); - let title = item.attributes.title; - let link = item.attributes.path.alias; - let image = ""; - let articleSummarySize = "col-md-12"; + //Date - make human readable + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + let date = new Date(item.attributes.created).toLocaleDateString('en-us', options); + let title = item.attributes.title; + let link = item.attributes.path.alias; + let image = ""; + let articleSummarySize = "col-md-12"; - var articleRow = document.createElement('div') - articleRow.className = 'ucb-article-card row' + var articleRow = document.createElement('div') + articleRow.className = 'ucb-article-card row' - if(link && imageSrc) { + if (link && imageSrc) { articleSummarySize = "col-md-10" var imgContainer = document.createElement('div') @@ -225,7 +227,8 @@ function renderArticleList( JSONURL, ExcludeCategories = "", ExcludeTags = "") { imgLink.href = link; var articleImg = document.createElement('img') - articleImg.src = imageSrc; + articleImg.src = imageSrc.src; + articleImg.setAttribute('alt', imageSrc.alt); imgLink.appendChild(articleImg) imgContainer.appendChild(imgLink) @@ -238,7 +241,7 @@ function renderArticleList( JSONURL, ExcludeCategories = "", ExcludeTags = "") { // Container var articleDataContainer = document.createElement('div') articleDataContainer.className = `col-sm-12 ${articleSummarySize} ucb-article-card-data` - + // Header var articleDataLink = document.createElement('a') @@ -266,7 +269,7 @@ function renderArticleList( JSONURL, ExcludeCategories = "", ExcludeTags = "") { // Read more & link var articleSummaryReadMore = document.createElement('span') articleSummaryReadMore.className = 'ucb-article-card-more' - + var readMoreLink = document.createElement('a') readMoreLink.href = link; readMoreLink.innerText = `Read more` @@ -284,41 +287,41 @@ function renderArticleList( JSONURL, ExcludeCategories = "", ExcludeTags = "") { articleDataContainer.appendChild(articleCardDate) articleDataContainer.appendChild(articleSummaryBody) articleDataContainer.appendChild(articleSummaryReadMore) - + articleRow.appendChild(articleDataContainer) - let dataOutput = document.getElementById("ucb-al-data"); - let thisArticle = document.createElement("article"); - thisArticle.className = 'ucb-article-card-container'; - thisArticle.appendChild(articleRow); - dataOutput.append(thisArticle); + let dataOutput = document.getElementById("ucb-al-data"); + let thisArticle = document.createElement("article"); + thisArticle.className = 'ucb-article-card-container'; + thisArticle.appendChild(articleRow); + dataOutput.append(thisArticle); - if(NEXTJSONURL){ - toggleMessage('ucb-el-load-more', 'inline-block') + if (NEXTJSONURL) { + toggleMessage('ucb-el-load-more', 'inline-block') + } } - } - }) - - // done loading -- hide the loading spinner graphic - toggleMessage("ucb-al-loading", "none"); - resolve(NEXTJSONURL); - }).catch(function(error) { - // catch any fetch errors and let the user know so they're not endlessly watching the spinner - console.log("Fetch Error in URL : " + JSONURL); - console.log("Fetch Error is : " + error); - // turn off spinner - toggleMessage("ucb-al-loading", "none"); - // turn on default error message - if(error){ - toggleMessage("ucb-al-error", "block"); + }) - } + // done loading -- hide the loading spinner graphic + toggleMessage("ucb-al-loading", "none"); + resolve(NEXTJSONURL); + }).catch(function (error) { + // catch any fetch errors and let the user know so they're not endlessly watching the spinner + console.log("Fetch Error in URL : " + JSONURL); + console.log("Fetch Error is : " + error); + // turn off spinner + toggleMessage("ucb-al-loading", "none"); + // turn on default error message + if (error) { + toggleMessage("ucb-al-error", "block"); + + } - }); + }); - } + } }); } @@ -339,10 +342,10 @@ function renderArticleList( JSONURL, ExcludeCategories = "", ExcludeTags = "") { CategoryExclude = el.dataset.excats; TagsExclude = el.dataset.extags; } - + // attempt to render the data requested - renderArticleList( JSONURL, CategoryExclude, TagsExclude,).then((response) => { - if(response) { + renderArticleList(JSONURL, CategoryExclude, TagsExclude,).then((response) => { + if (response) { NEXTJSONURL = "/jsonapi/" + response; } }); @@ -350,17 +353,17 @@ function renderArticleList( JSONURL, ExcludeCategories = "", ExcludeTags = "") { // watch for scrolling and determine if we're at the bottom of the content and need to request more const button = document.getElementById('ucb-el-load-more') button.addEventListener("click", function () { - if(NEXTJSONURL) { - renderArticleList( NEXTJSONURL, CategoryExclude, TagsExclude,).then((response) => { - if(response) { - NEXTJSONURL = "/jsonapi/" + response; - loadingData = false; - } else { - NEXTJSONURL = ""; - toggleMessage("ucb-al-end-of-data", "block"); - toggleMessage('ucb-el-load-more') - } - }); - } + if (NEXTJSONURL) { + renderArticleList(NEXTJSONURL, CategoryExclude, TagsExclude,).then((response) => { + if (response) { + NEXTJSONURL = "/jsonapi/" + response; + loadingData = false; + } else { + NEXTJSONURL = ""; + toggleMessage("ucb-al-end-of-data", "block"); + toggleMessage('ucb-el-load-more') + } + }); + } }) })() diff --git a/templates/content/node--ucb-article.html.twig b/templates/content/node--ucb-article.html.twig index ab604a5b..2544eb8f 100644 --- a/templates/content/node--ucb-article.html.twig +++ b/templates/content/node--ucb-article.html.twig @@ -143,10 +143,12 @@

{{ label }}

{{ content.field_article_title_background }} -
- - {{ label }} - + +
+

+ {{ label }} +

+
{% endif %}