From b3727ad7f94e53281f58e9601e3629d8e8a78cf7 Mon Sep 17 00:00:00 2001 From: Travis Doughty Date: Thu, 29 Aug 2024 13:12:17 -0400 Subject: [PATCH] Clean up and code walkthrough --- content/body/load-more.php | 98 +++++++++++++++------------ content/bottom/load-more.php | 2 +- js/demos/load-more/data.js | 68 +++++++++++++++++++ js/demos/{ => load-more}/load-more.js | 86 ++++------------------- 4 files changed, 137 insertions(+), 117 deletions(-) create mode 100644 js/demos/load-more/data.js rename js/demos/{ => load-more}/load-more.js (66%) diff --git a/content/body/load-more.php b/content/body/load-more.php index 195ba650..225aee31 100644 --- a/content/body/load-more.php +++ b/content/body/load-more.php @@ -4,18 +4,17 @@ "This is the best solution to use, especially when building from scratch.", ]); ?> -

In a world of everything online and always connected, companies are on a mission to keep you browsing their content for as - long as they possibly can. Many patterns have come about for how to handle paginated content. Since the introduction of - social media one pattern in particular has been a mainstay for wasting a users time. The infinite scroll pattern. While it's good -to keep folks endlessly scrolling content on a page it's not very accessible, it may be hampering the user experience and even hurting -the ultimate goal of your website.

+

In today's digital age, where everything is online and always connected, companies strive to keep users engaged with their content for as long as possible. Various patterns have emerged for managing paginated content, and since the advent of social media, one particular pattern has become prevalent: infinite scroll. While this approach may be beneficial for companies looking to increase user engagement by encouraging endless scrolling, it is not always the most accessible or user-friendly option. In some cases, it can even undermine the primary objectives of a website.

-

Consider reading this article by Nieman Norman Group https://www.nngroup.com/articles/infinite-scrolling/ -which talks about the detriments of a user experience that features infinite scroll.

+

In the following demos, we will focus on an alternative to infinite scroll: the "load more" button. This method prevents users from becoming trapped in an endless loop of content, allowing them to easily navigate the rest of the site. It's worth noting that other alternatives, such as traditional pagination, also exist, but for the purposes of these demos, our emphasis will be on the load more button.

+ +

For a deeper understanding of the drawbacks of infinite scrolling on user experience, consider reading this insightful article by the Nielsen Norman Group: https://www.nngroup.com/articles/infinite-scrolling/

+ +

The following demonstrations use placeholder assets sourced from https://dev.me/products/image-placeholder.

Category Grid Load More Example

-

Category grid's are relatively simple components

+

Category grids are typically straightforward components. However, incorporating a "load more" button introduces additional complexity, particularly when ensuring that these components remain accessible. The key takeaway from this example is understanding how the "load more" functionality operates. Notably, when additional category tiles are loaded, the user's focus is returned to the first tile in the newly loaded set. This approach allows users to seamlessly continue from where they left off before clicking the "load more" button.

Showing 3 of 9 Categories

@@ -59,14 +58,34 @@ { "replaceHtmlRules": {}, "steps": [{ - "label": "Ensure details has open attribute set", - "highlight": "open", - "notes": "This ensures the animated version is shown by default." + "label": "Add a product counter and add the aria-live attribute", + "highlight": "%OPENCLOSECONTENTTAG%p id=\"view-count\"", + "notes": "It's a good idea to tell users how many products they're viewing out of the total number of products. We add the aria-live attribute set to polite to ensure that when it updates, it will be read out by the screen reader after the focused element is read." + }, + { + "label": "Ensure all images have alt tags", + "highlight": "alt", + "notes": "It's important to make sure that all images have a descriptive alt tag." + }, + { + "label": "Ensure anchor tags contain both the action and the category name", + "highlight": "%OPENCLOSETAG%a ||| %OPENCLOSECONTENTTAG%a", + "notes": "It's important that the user knows what action the anchor tags will perform by clicking on them. In our case we've inlcuded the word \"Shop\" in front of the category name. This tells the user they'll be linked to a shops page for the category. For this example we're linking to \"/\", in a real world application this should go to the category page." }, { - "label": "Ensure the summary tag has the correct aria-label", - "highlight": "aria-label", - "notes": "After doing this step, make sure all other steps in example 1 above are followed." + "label": "Ensure buttons contain the action and the descriptor", + "highlight": "%OPENCLOSECONTENTTAG%button id=\"view-more-btn\"", + "notes": "Make sure that you're including both the action and a descriptor for buttons. This tells the user what they can expect by clicking the button." + }, + { + "label": "Ensure the focus goes to the proper tile after clicking View More Categories button.", + "highlight": "%FILE% ./js/demos/load-more/load-more.js ~ \\s*const productTiles[\\s\\S]*?\\}\\)\\;", + "notes": "It's important to move the users focus state to the first item in the newly loaded products. For demo purposes we're including a reset button. In the real world this data would likely come from an API request. By setting the users focus to the first item in the new batch of products a user can keep browsing through the tiles where they left off." + }, + { + "label": "Update the product counter when loading more", + "highlight": "%FILE% ./js/demos/load-more/load-more.js ~ \\s*this.setCount[\\s\\S]*?\\)\\;", + "notes": "We're calling a counter update function in the demo to change the text of the \"Showing X of X Categories\" text to update the product count the user is viewing. Remember that we added aria-live=\"polite\" to this text in step 1. This will announce the update to the user. In a real application this data would likely come from an API." } ] } @@ -74,9 +93,9 @@

Product Listing Page Load More Example

-

This is the preferred way of implementing loading more content in an accessible way that aids goal-oriented finding tasks.

+

Product listing pages with "load more" functionality can become quite complex. Typically, each product tile not only provides a clickable link to a detailed product page but also includes several other interactive elements. For example, you may have product ratings, an "add to favorites" button, an "add to cart" button, and other interactive features, all of which need to be made accessible.

-

The following demo makes use of placeholder assets pulled from https://dev.me/products/image-placeholder

+

The following example is relatively basic but demonstrates how you might structure your product tiles. A key consideration is how the tile elements are grouped using role="group" along with aria-labelledby="productNameId", ensuring accessibility and ease of navigation. Additionally, when more product tiles are loaded, the user's focus is returned to the first tile in the newly loaded set. This feature allows users to seamlessly continue their browsing experience from where they left off before clicking the "load more" button.

Showing 3 of 9 Products

@@ -125,44 +144,39 @@ { "replaceHtmlRules": {}, "steps": [{ - "label": "Put in details and summary tag structure in HTML", - "highlight": "%OPENCLOSETAG%details ||| %OPENCLOSECONTENTTAG%summary", - "notes": "" + "label": "Add a product counter and add the aria-live attribute", + "highlight": "%OPENCLOSECONTENTTAG%p id=\"product-count\"", + "notes": "It's a good idea to tell users how many products they're viewing out of the total number of products. We add the aria-live attribute set to polite to ensure that when it updates, it will be read out by the screen reader after the focused element is read." }, { - "label": "Place role of button inside the summary", - "highlight": "role", - "notes": "This is to ensure iOS reports this correctly to VoiceOver" - }, - { - "label": "Put animated GIF after the summary tag and the poster image of the animation just before the details tag", - "highlight": "%OPENTAG%img", - "notes": "Note that the div surrounding the animated GIF is there for styling purposes. It ensures that, when the summary widget is expanded, the animated image is placed over the still poster" + "label": "Ensure all images have alt tags", + "highlight": "alt", + "notes": "It's important to make sure that all images have a descriptive alt tag." }, { - "label": "Don't forget the alternative text for the images!", - "highlight": "alt", - "notes": "Alternative text describes these images to screen reader users in reading mode." + "label": "Ensure anchor tags contain both the action and the category name", + "highlight": "%OPENCLOSETAG%a ||| %OPENCLOSECONTENTTAG%a", + "notes": "It's important that the user knows what action the anchor tags will perform by clicking on them. In our case we've inlcuded the word \"Shop\" in front of the category name. This tells the user they'll be linked to a shops page for the category. For this example we're linking to \"/\", in a real world application this should go to the category page." }, { - "label": "Add lazy attributes to images", - "highlight": "loading", - "notes": "This adds a performance boost by only showing the image when it is visible in the browser viewport." + "label": "Ensure buttons contain the action and the descriptor", + "highlight": "%OPENCLOSETAG%button ||| %OPENCLOSECONTENTTAG%button", + "notes": "Make sure that you're including both the action and a descriptor for buttons. This tells the user what they can expect by clicking the button." }, { - "label": "Put aria label inside of summary tag", - "highlight": "aria-label", - "notes": "Note that when the summary is opened, this aria-label must be changed to 'pause' and 'play' when it is closed again." + "label": "Ensure the product tile contains the proper role and aria-labelledby", + "highlight": "%OPENTAG%div class=\"product-tile\"", + "notes": "Since the product tiles contain more than 1 interactive element, it's important to add a role=\"group\" along with an aria-labelledby which points to the ID of the product name. This will ensure that when a user focuses one of the interactive elements, the screen reading will announce that these elements are apart of the group labelled by the product name. This is helpful for when a user tabs past the Add to Cart button and then shift tabs backwords. Without this the user may not know what they are adding to cart." }, { - "label": "Add small accessibility extras via JavaScript", - "highlight": "%FILE% js/modules/enable-animatedGif.js", - "notes": "I added extra JavaScript to the original example to handle: " + "label": "Ensure the focus goes to the proper tile after clicking Load More Products button", + "highlight": "%FILE% ./js/demos/load-more/load-more.js ~ \\s*const productTiles[\\s\\S]*?\\}\\)\\;", + "notes": "It's important to move the users focus state to the first item in the newly loaded products. For demo purposes we're including a reset button. In the real world this data would likely come from an API request. By setting the users focus to the first item in the new batch of products a user can keep browsing through the tiles where they left off." }, { - "label": "Add support for Enable's Pause Animation Control", - "highlight": "%FILE% js/modules/enable-animatedGif.js ~ document.addEventListener\\('enable-play-animations'[^\\)]*\\);", - "notes": "I added extra JavaScript to the original example to handle: " + "label": "Update the product counter when loading more", + "highlight": "%FILE% ./js/demos/load-more/load-more.js ~ \\s*this.setCount[\\s\\S]*?\\)\\;", + "notes": "We're calling a counter update function in the demo to change the text of the \"Showing X of X Products\" text to update the product count the user is viewing. Remember that we added aria-live=\"polite\" to this text in step 1. This will announce the update to the user. In a real application this data would likely come from an API." } ] } diff --git a/content/bottom/load-more.php b/content/bottom/load-more.php index 2223f2e6..775ad3ba 100644 --- a/content/bottom/load-more.php +++ b/content/bottom/load-more.php @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/js/demos/load-more/data.js b/js/demos/load-more/data.js new file mode 100644 index 00000000..f180f43a --- /dev/null +++ b/js/demos/load-more/data.js @@ -0,0 +1,68 @@ +export const resetProductsData = [ + { + id: 'product-1', + name: 'Modern Tufted Armless Lounge Chair', + image: 'images/load-more/chair1.png', + price: '$399.99', + category: 'Modern Chairs', + }, + { + id: 'product-2', + name: 'Minimalist Felt Lounge Chair', + image: 'images/load-more/chair2.png', + price: '$199.99', + category: 'Simple Chairs', + }, + { + id: 'product-3', + name: 'Classic Tufted Leather Wingback Chair', + image: 'images/load-more/chair3.png', + price: '$799.99', + category: 'Leather Chairs', + }, +]; + +export const productsData = [ + { + id: 'product-4', + name: 'Wire Frame Accent Chair', + image: 'images/load-more/chair4.png', + price: '$99.99', + category: 'Metal Chairs', + }, + { + id: 'product-5', + name: 'Modern Wooden End Table', + image: 'images/load-more/chair5.png', + price: '$149.99', + category: 'End Tables', + }, + { + id: 'product-6', + name: 'Ergonomic Leather Office Chair', + image: 'images/load-more/chair6.png', + price: '$299.99', + category: 'Office Chairs', + }, + { + id: 'product-7', + name: 'Modern Wood Slat Back Dining Chair', + image: 'images/load-more/chair7.png', + price: '$99.99', + category: 'Wooden Chairs', + }, + { + id: 'product-8', + name: 'Wooden Picnic Table with Benches', + image: 'images/load-more/chair8.png', + price: '$999.99', + category: 'Outdoor Chairs', + }, + { + id: 'product-9', + name: 'Classic Windsor Chair', + image: 'images/load-more/chair9.png', + price: '$149.99', + category: 'Kitchen Chairs', + }, +]; diff --git a/js/demos/load-more.js b/js/demos/load-more/load-more.js similarity index 66% rename from js/demos/load-more.js rename to js/demos/load-more/load-more.js index eaf28f03..7b78094e 100644 --- a/js/demos/load-more.js +++ b/js/demos/load-more/load-more.js @@ -1,71 +1,4 @@ -const resetProductsData = [ - { - id: 'product-1', - name: 'Modern Tufted Armless Lounge Chair', - image: 'images/load-more/chair1.png', - price: '$399.99', - category: 'Modern Chairs', - }, - { - id: 'product-2', - name: 'Minimalist Felt Lounge Chair', - image: 'images/load-more/chair2.png', - price: '$199.99', - category: 'Simple Chairs', - }, - { - id: 'product-3', - name: 'Classic Tufted Leather Wingback Chair', - image: 'images/load-more/chair3.png', - price: '$799.99', - category: 'Leather Chairs', - }, -]; - -const productsData = [ - { - id: 'product-4', - name: 'Wire Frame Accent Chair', - image: 'images/load-more/chair4.png', - price: '$99.99', - category: 'Metal Chairs', - }, - { - id: 'product-5', - name: 'Modern Wooden End Table', - image: 'images/load-more/chair5.png', - price: '$149.99', - category: 'End Tables', - }, - { - id: 'product-6', - name: 'Ergonomic Leather Office Chair', - image: 'images/load-more/chair6.png', - price: '$299.99', - category: 'Office Chairs', - }, - { - id: 'product-7', - name: 'Modern Wood Slat Back Dining Chair', - image: 'images/load-more/chair7.png', - price: '$99.99', - category: 'Wooden Chairs', - }, - { - id: 'product-8', - name: 'Wooden Picnic Table with Benches', - image: 'images/load-more/chair8.png', - price: '$999.99', - category: 'Outdoor Chairs', - }, - { - id: 'product-9', - name: 'Classic Windsor Chair', - image: 'images/load-more/chair9.png', - price: '$149.99', - category: 'Kitchen Chairs', - }, -]; +import { productsData, resetProductsData } from './data.js'; class ExampleGrid { constructor({ @@ -155,9 +88,12 @@ class ExampleGrid { this.setCount(this.itemCountLoadMore); - const items = document.querySelectorAll(`.${this.focusClass}`); - items[this.itemCountLoadMore - this.itemPerPage].focus(); - items[this.itemCountLoadMore - this.itemPerPage].scrollIntoView({ + const productTiles = document.querySelectorAll(`.${this.focusClass}`); + const tileToTarget = + productTiles[this.itemCountLoadMore - this.itemPerPage]; + + tileToTarget.focus(); + tileToTarget.scrollIntoView({ block: 'center', behavior: 'smooth', }); @@ -175,9 +111,11 @@ class ExampleGrid { this.loadMoreBtn.classList.remove('hide-btn'); this.resetBtn.classList.add('hide-btn'); - const items = document.querySelectorAll(`.${this.focusClass}`); - items[0].focus(); - items[0].scrollIntoView({ block: 'center', behavior: 'smooth' }); + const productTiles = document.querySelectorAll(`.${this.focusClass}`); + const tileToTarget = productTiles[0]; + + tileToTarget.focus(); + tileToTarget.scrollIntoView({ block: 'center', behavior: 'smooth' }); } setCount(itemCount) {