diff --git a/content/body/load-more.php b/content/body/load-more.php new file mode 100644 index 00000000..86e8aaf2 --- /dev/null +++ b/content/body/load-more.php @@ -0,0 +1,183 @@ + true, + "comment" => + "This is the best solution to use, especially when building from scratch.", +]); ?> + +

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.

+ +

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: Infinite Scrolling Is Not for Every Website

+ +

The following demonstrations use placeholder assets sourced from The Dev.me Image Placeholder API.

+ +

Category Grid Load More Example

+ +

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

+ +
+ + + + + +
+ + + +
+ + + + + +

Product Listing Page Load More Example

+ +

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 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

+ +
+
+ + Modern Tufted Armless Lounge Chair + + Modern Tufted Armless Lounge Chair +

Modern Tufted Armless Lounge Chair

+

$399.99

+ +
+ +
+ + Minimalist Felt Lounge Chair + + Minimalist Felt Lounge Chair +

Minimalist Felt Lounge Chair

+

$199.99

+ +
+ +
+ + Classic Tufted Leather Wingback Chair + + Classic Tufted Leather Wingback Chair +

Classic Tufted Leather Wingback Chair

+

$799.99

+ +
+
+ + + +
+ + + + + + \ No newline at end of file diff --git a/content/bottom/load-more.php b/content/bottom/load-more.php new file mode 100644 index 00000000..775ad3ba --- /dev/null +++ b/content/bottom/load-more.php @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/content/head/load-more.php b/content/head/load-more.php new file mode 100644 index 00000000..9e516124 --- /dev/null +++ b/content/head/load-more.php @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/css/alert copy.css b/css/alert copy.css new file mode 100644 index 00000000..e69de29b diff --git a/css/image-gallery.css b/css/image-gallery.css index 842818a2..11a465fe 100644 --- a/css/image-gallery.css +++ b/css/image-gallery.css @@ -39,6 +39,12 @@ border: none; background: transparent; } +.enable__is-dark-mode .gallery .thumb-nav-button { + filter: invert(100%); +} +.enable__is-dark-mode .gallery .thumbnail-button { + border: solid 1px #ededed; +} .gallery .thumbnail-slider { display: flex; align-items: center; @@ -54,8 +60,11 @@ width: 60px; height: 40px; cursor: pointer; - opacity: 0.6; border-radius: 5px; + margin-right: 5px; +} +.gallery .thumbnail-slider .thumbnails .thumbnail-button:last-child { + margin-right: 0; } .gallery .thumbnail-slider .thumbnails .thumbnail-button img { width: 100%; @@ -65,13 +74,23 @@ .gallery .thumbnail-slider .thumbnails .thumbnail-button img:focus-visible { outline: none; } +.gallery .thumbnail-slider .thumbnails .thumbnail-button:hover { + border: 0.1px solid #292929; +} +.enable__is-dark-mode .gallery .thumbnail-slider .thumbnails .thumbnail-button:hover { + border: 0.38px solid #9a6c6c; +} .gallery .thumbnail-slider .thumbnails .thumbnail-button.active { opacity: 1; border: 0.38px solid #2b2b2b; + border-bottom-width: 3px; } -.gallery .thumbnail-slider .thumbnails .thumbnail-button:hover { - opacity: 0.4; - border: 0.1px solid #2b2b2b; +.gallery .thumbnail-slider .thumbnails .thumbnail-button.active img { + height: calc(100% + 3px); +} +.enable__is-dark-mode .gallery .thumbnail-slider .thumbnails .thumbnail-button.active { + border: 0.38px solid #9a6c6c; + border-bottom-width: 3px; } .gallery .thumbnail-slider .thumbnails .thumbnail-button:focus-visible { margin: 3px; diff --git a/css/load-more.css b/css/load-more.css new file mode 100644 index 00000000..35ecf289 --- /dev/null +++ b/css/load-more.css @@ -0,0 +1,129 @@ +:root { + --focus-color: #097efb; + --focus-color-dark-bg: #3b99fc; +} +@media (prefers-color-scheme: dark) { + :root { + --focus-color: #3b99fc; + --focus-color-dark-bg: #097efb; + } +} +#product-grid, +#view-grid { + width: 100%; + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + justify-content: center; +} +.product-tile, +.view-tile { + position: relative; + width: 33.3333%; + box-sizing: border-box; + display: inline-block; + padding: 20px; +} +.product-tile img, +.view-tile img { + max-width: 100%; + border: 1px solid #ccc; + padding: 20px; +} +@media only screen and (min-width: 1px) and (max-width: 719px) { + .product-tile, + .view-tile { + width: 100%; + } +} +.product-name { + font-size: 1rem; +} +.product-price { + font-size: 1.25rem; + font-weight: bold; + margin-bottom: 15px; +} +.product-details-link { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} +.view-details-link { + position: absolute; + font-size: 1rem; + line-height: 2.75rem; + left: 0; + bottom: 0; + font-weight: bold; + width: 100%; + background: #0c3f75; + color: #fff; + min-height: 44px; + border: none; + outline-offset: 4px; + text-align: center; + text-decoration: none; +} +@media only screen and (min-width: 720px) and (max-width: 959px) { + .view-details-link { + font-size: 0.75rem; + line-height: 2.125rem; + min-height: 34px; + } +} +#product-count, +#category-count { + text-align: center; +} +#load-more-btn, +#display-more-btn { + font-size: 0.9375rem; + font-weight: bold; + min-width: 200px; + min-height: 44px; + display: block; + cursor: pointer; + background: #3a4b5c; + color: #fff; + border: none; + margin: 10px auto; + outline-offset: 4px; +} +#product-reset-btn, +#display-reset-btn { + font-size: 0.9375rem; + font-weight: bold; + min-width: 200px; + min-height: 44px; + display: block; + cursor: pointer; + background: none; + border: none; + margin: 10px auto; + outline-offset: 4px; + text-decoration: underline; +} +.enable__is-dark-mode #product-reset-btn, +.enable__is-dark-mode #display-reset-btn { + color: #ededed; +} +.add-to-cart-btn { + font-size: 0.9375rem; + font-weight: bold; + min-height: 44px; + background: #0c3f75; + color: #fff; + border: none; + padding: 0 15px; + outline-offset: 4px; +} +.hide-btn { + display: none !important; +} +.tile-relative { + position: relative; +} +/*# sourceMappingURL=load-more.css.map */ \ No newline at end of file diff --git a/images/load-more/chair1.png b/images/load-more/chair1.png new file mode 100644 index 00000000..d7a7bf8c Binary files /dev/null and b/images/load-more/chair1.png differ diff --git a/images/load-more/chair2.png b/images/load-more/chair2.png new file mode 100644 index 00000000..7205ec84 Binary files /dev/null and b/images/load-more/chair2.png differ diff --git a/images/load-more/chair3.png b/images/load-more/chair3.png new file mode 100644 index 00000000..3749854b Binary files /dev/null and b/images/load-more/chair3.png differ diff --git a/images/load-more/chair4.png b/images/load-more/chair4.png new file mode 100644 index 00000000..096f5c27 Binary files /dev/null and b/images/load-more/chair4.png differ diff --git a/images/load-more/chair5.png b/images/load-more/chair5.png new file mode 100644 index 00000000..278b1e49 Binary files /dev/null and b/images/load-more/chair5.png differ diff --git a/images/load-more/chair6.png b/images/load-more/chair6.png new file mode 100644 index 00000000..dda4b98b Binary files /dev/null and b/images/load-more/chair6.png differ diff --git a/images/load-more/chair7.png b/images/load-more/chair7.png new file mode 100644 index 00000000..cfcbcdd2 Binary files /dev/null and b/images/load-more/chair7.png differ diff --git a/images/load-more/chair8.png b/images/load-more/chair8.png new file mode 100644 index 00000000..8ba6dccf Binary files /dev/null and b/images/load-more/chair8.png differ diff --git a/images/load-more/chair9.png b/images/load-more/chair9.png new file mode 100644 index 00000000..541a5d0a Binary files /dev/null and b/images/load-more/chair9.png differ diff --git a/images/main-menu/load-more.png b/images/main-menu/load-more.png new file mode 100644 index 00000000..e4559c63 Binary files /dev/null and b/images/main-menu/load-more.png differ diff --git a/images/main-menu/load-more.webp b/images/main-menu/load-more.webp new file mode 100644 index 00000000..0f04b1de Binary files /dev/null and b/images/main-menu/load-more.webp differ diff --git a/images/posters/load-more.jpg b/images/posters/load-more.jpg new file mode 100644 index 00000000..df8e0de6 Binary files /dev/null and b/images/posters/load-more.jpg differ 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/load-more.js b/js/demos/load-more/load-more.js new file mode 100644 index 00000000..8670b035 --- /dev/null +++ b/js/demos/load-more/load-more.js @@ -0,0 +1,149 @@ +import { productsData, resetProductsData } from './data.js'; + +class ExampleGrid { + constructor({ + type, + products, + resetProducts, + loadMoreBtn, + resetBtn, + productGrid, + countText, + tileClass, + focusClass, + }) { + this.type = type; + this.products = products; + this.resetProducts = resetProducts; + this.loadMoreBtn = document.getElementById(loadMoreBtn); + this.resetBtn = document.getElementById(resetBtn); + this.productGrid = document.getElementById(productGrid); + this.countText = document.getElementById(countText); + this.tileClass = tileClass; + this.focusClass = focusClass; + this.itemCount = 0; + this.itemCountLoadMore = 3; + this.itemPerPage = 3; + this.itemTotal = 9; + + this.configure(); + } + + configure() { + this.loadMoreBtn.addEventListener('click', this.loadMore.bind(this)); + this.resetBtn.addEventListener('click', this.reset.bind(this)); + } + + createProductHtml(product) { + switch (this.type) { + case 'product-grid': + return ` +
+ + ${product.name} + + ${product.name} +

${product.name}

+

${product.price}

+ +
+ `; + case 'view-grid': + return ` +
+
+ ${product.name} + + Shop ${product.category} + +
+
+ `; + default: + return new Error(`Type of ${this.type} is not supported`); + } + } + + generateTiles(productData) { + productData.forEach((product) => { + const productHtml = this.createProductHtml(product); + this.productGrid.insertAdjacentHTML('beforeend', productHtml); + }); + } + + loadMore() { + const productData = this.products.slice( + this.itemCount, + this.itemCount + this.itemPerPage, + ); + this.generateTiles(productData); + + this.itemCount += 3; + this.itemCountLoadMore += 3; + + if (this.itemCount === this.itemTotal - this.itemPerPage) { + this.loadMoreBtn.classList.add('hide-btn'); + this.resetBtn.classList.remove('hide-btn'); + } + + this.setCount(this.itemCountLoadMore); + + const productTiles = document.querySelectorAll(`.${this.focusClass}`); + const tileToTarget = + productTiles[this.itemCountLoadMore - this.itemPerPage]; + + tileToTarget.focus(); + tileToTarget.scrollIntoView({ + block: 'center', + behavior: 'smooth', + }); + } + + reset() { + this.productGrid.innerHTML = ''; + this.generateTiles(this.resetProducts); + + this.itemCount = 0; + this.itemCountLoadMore = 3; + + this.setCount(this.itemCountLoadMore); + + this.loadMoreBtn.classList.remove('hide-btn'); + this.resetBtn.classList.add('hide-btn'); + + const productTiles = document.querySelectorAll(`.${this.focusClass}`); + const tileToTarget = productTiles[0]; + + tileToTarget.focus(); + tileToTarget.scrollIntoView({ block: 'center', behavior: 'smooth' }); + } + + setCount(itemCount) { + const type = this.type === 'product-grid' ? 'Products' : 'Categories'; + this.countText.innerText = `Showing ${itemCount} of 9 ${type}`; + } +} + +new ExampleGrid({ + type: 'product-grid', + products: productsData, + resetProducts: resetProductsData, + loadMoreBtn: 'load-more-btn', + resetBtn: 'product-reset-btn', + productGrid: 'product-grid', + countText: 'product-count', + tileClass: 'product-tile', + focusClass: 'product-details-link', +}); + +new ExampleGrid({ + type: 'view-grid', + products: productsData, + resetProducts: resetProductsData, + loadMoreBtn: 'display-more-btn', + resetBtn: 'display-reset-btn', + productGrid: 'view-grid', + countText: 'category-count', + tileClass: 'view-tile', + focusClass: 'view-details-link', +}); diff --git a/js/enable-libs/showcode.js b/js/enable-libs/showcode.js index 7244cac5..0cd3d688 100755 --- a/js/enable-libs/showcode.js +++ b/js/enable-libs/showcode.js @@ -347,6 +347,9 @@ const showcode = new function () { // Will show HTML encased in tag with given id // "%OUTERHTML% id" // Will show HTML encased in tag with given id (including the tag) + // "%FILE% filePath" + // Will allow you to change the code block to a file of your choosing by passing the URL path to the file + // this is useful when you want to show a part of another file e.g. a JavaScript file command = highlightString.match(commandsRe); diff --git a/js/test/load-more.test.js b/js/test/load-more.test.js new file mode 100644 index 00000000..4348c4e4 --- /dev/null +++ b/js/test/load-more.test.js @@ -0,0 +1,247 @@ +'use strict'; +import config from './test-config.js'; +import testHelpers from './test-helpers.js'; + +const exampleImages1 = '#example1 img'; +const exampleImages2 = '#example2 img'; + +describe('Load More Tests', () => { + it('Example 1 should have images with non-empty alt tags', async () => { + await page.goto(`${config.BASE_URL}/load-more.php`); + + await page.waitForSelector('#example1'); + + const images = await page.$$eval(exampleImages1, (imgs) => + imgs.map((img) => ({ + src: img.src, + alt: img.alt, + })), + ); + + images.forEach((image) => { + expect(image.src).not.toBe(''); + expect(image.alt).not.toBe(''); + }); + }); + + it('Example 1 should initalize with "Showing 3 of 9 Categories" and have aria-live polite', async () => { + await page.goto(`${config.BASE_URL}/load-more.php`); + + await page.waitForSelector('#example1'); + + const domInfo = await page.evaluate(() => { + const categoryCount = document.getElementById('category-count'); + + return { + hasProperCountText: + categoryCount.innerText === 'Showing 3 of 9 Categories', + hasAriaLive: + categoryCount.getAttribute('aria-live') === 'polite', + }; + }); + + expect(domInfo.hasProperCountText).toBe(true); + expect(domInfo.hasAriaLive).toBe(true); + }); + + it('Example 1 keyboard support for "View More Categories" and "Reset Category Grid Demo" buttons', async () => { + const numTabPress = 3; + await page.goto(`${config.BASE_URL}/load-more.php`); + + await page.waitForSelector('#example1'); + + let domInfo; + + // Start on the first category tile + domInfo = await page.evaluate(() => { + const firstTile = document.querySelector('.view-details-link'); + firstTile.focus(); + return { + isFirstTileFocused: document.activeElement === firstTile, + }; + }); + + expect(domInfo.isFirstTileFocused).toBe(true); + + // Tab to the "View More Categories" button and press it to load the first new set + + // for (let i = 0; i < numTabPress; i++) { + // await page.keyboard.press('Tab'); + // } + testHelpers.keyPressHelper(page, 'Tab', numTabPress); + await page.keyboard.press('Enter'); + + domInfo = await page.evaluate(() => { + const allTiles = Array.from( + document.querySelectorAll('.view-details-link'), + ); + return { + isFirstTileInNewSetFocused: + document.activeElement === allTiles[3], + }; + }); + + expect(domInfo.isFirstTileInNewSetFocused).toBe(true); + + // Expect the counter to update + domInfo = await page.evaluate(() => { + const countText = document.querySelector('#category-count'); + return { + isCountTextUpdated: + countText.innerHTML === 'Showing 6 of 9 Categories', + }; + }); + + expect(domInfo.isCountTextUpdated).toBe(true); + + // Tab to the "View More Categories" button and press it to load the second new set + testHelpers.keyPressHelper(page, 'Tab', numTabPress); + await page.keyboard.press('Enter'); + + // Tab to the "Reset Category Grid Demo" + testHelpers.keyPressHelper(page, 'Tab', numTabPress); + await page.keyboard.press('Enter'); + + // Expect the first tile to be selected after resetting the demo + domInfo = await page.evaluate(() => { + const allTiles = Array.from( + document.querySelectorAll('.view-details-link'), + ); + return { + isFocusReset: document.activeElement === allTiles[0], + }; + }); + + expect(domInfo.isFocusReset).toBe(true); + }); + + it('Example 2 should have images with non-empty alt tags', async () => { + await page.goto(`${config.BASE_URL}/load-more.php`); + + await page.waitForSelector('#example2'); + + const images = await page.$$eval(exampleImages2, (imgs) => + imgs.map((img) => ({ + src: img.src, + alt: img.alt, + })), + ); + + images.forEach((image) => { + expect(image.src).not.toBe(''); + expect(image.alt).not.toBe(''); + }); + }); + + it('Example 2 should have a proper role and aria-labelledby assigned', async () => { + await page.goto(`${config.BASE_URL}/load-more.php`); + + await page.waitForSelector('#example2'); + + const domInfo = await page.evaluate(() => { + const tiles = Array.from( + document.querySelectorAll('.product-tile'), + ); + + return { + hasGroupRole: tiles.every( + (tile) => tile.getAttribute('role') === 'group', + ), + hasAriaLabelledBy: tiles.every( + (tile) => tile.getAttribute('aria-labelledby') !== '', + ), + }; + }); + + expect(domInfo.hasGroupRole).toBe(true); + expect(domInfo.hasAriaLabelledBy).toBe(true); + }); + + it('Example 2 should initalize with "Showing 3 of 9 Products" and have aria-live polite', async () => { + await page.goto(`${config.BASE_URL}/load-more.php`); + + await page.waitForSelector('#example2'); + + const domInfo = await page.evaluate(() => { + const productCount = document.getElementById('product-count'); + + return { + hasProperCountText: + productCount.innerText === 'Showing 3 of 9 Products', + hasAriaLive: + productCount.getAttribute('aria-live') === 'polite', + }; + }); + + expect(domInfo.hasProperCountText).toBe(true); + expect(domInfo.hasAriaLive).toBe(true); + }); + + it('Example 2 keyboard support for "Load More Products" and "Reset Product Grid Demo" buttons', async () => { + const numTabPress = 6; + + await page.goto(`${config.BASE_URL}/load-more.php`); + + await page.waitForSelector('#example2'); + + let domInfo; + + // Start on the first category tile + domInfo = await page.evaluate(() => { + const firstTile = document.querySelector('.product-details-link'); + firstTile.focus(); + return { + isFirstTileFocused: document.activeElement === firstTile, + }; + }); + + expect(domInfo.isFirstTileFocused).toBe(true); + + // Tab to the "View More Categories" button and press it to load the first new set + testHelpers.keyPressHelper(page, 'Tab', numTabPress); + await page.keyboard.press('Enter'); + + domInfo = await page.evaluate(() => { + const allTiles = Array.from( + document.querySelectorAll('.product-details-link'), + ); + return { + isFirstTileInNewSetFocused: + document.activeElement === allTiles[3], + }; + }); + + expect(domInfo.isFirstTileInNewSetFocused).toBe(true); + + // Expect the counter to update + domInfo = await page.evaluate(() => { + const countText = document.querySelector('#product-count'); + return { + isCountTextUpdated: + countText.innerHTML === 'Showing 6 of 9 Products', + }; + }); + + expect(domInfo.isCountTextUpdated).toBe(true); + + // Tab to the "View More Categories" button and press it to load the second new set + testHelpers.keyPressHelper(page, 'Tab', numTabPress); + await page.keyboard.press('Enter'); + + // Tab to the "Reset Category Grid Demo" + testHelpers.keyPressHelper(page, 'Tab', numTabPress); + await page.keyboard.press('Enter'); + + // Expect the first tile to be selected after resetting the demo + domInfo = await page.evaluate(() => { + const allTiles = Array.from( + document.querySelectorAll('.product-details-link'), + ); + return { + isFocusReset: document.activeElement === allTiles[0], + }; + }); + + expect(domInfo.isFocusReset).toBe(true); + }); +}); diff --git a/js/test/test-helpers.js b/js/test/test-helpers.js index 82d3cc8b..96124b10 100644 --- a/js/test/test-helpers.js +++ b/js/test/test-helpers.js @@ -155,6 +155,12 @@ const testHelpers = new (function () { console.log(request.failure().errorText, request.url()); }); */ }; + + this.keyPressHelper = (page, key, numTimes) => { + for (let i = 0; i < numTimes; i++) { + page.keyboard.press(key); + } + }; })(); export default testHelpers; diff --git a/less/image-gallery.less b/less/image-gallery.less index 42695378..771eb75b 100644 --- a/less/image-gallery.less +++ b/less/image-gallery.less @@ -28,6 +28,18 @@ border: none; background: transparent; } + + .thumb-nav-button { + .enable__is-dark-mode & { + filter: invert(100%); + } + } + + .thumbnail-button { + .enable__is-dark-mode & { + border: solid 1px @dark-mode-white; + } + } .thumbnail-slider { display: flex; @@ -42,8 +54,13 @@ width: 60px; height: 40px; cursor: pointer; - opacity: 0.6; border-radius: 5px; + margin-right: 5px; + + &:last-child { + margin-right: 0; + } + img{ width: 100%; height: 100%; @@ -52,14 +69,30 @@ outline: none; } } + &:hover{ + + border: .1px solid rgb(41, 41, 41); + + .enable__is-dark-mode & { + border: .38px solid #9a6c6c; + } + } + &.active{ opacity: 1; border: .38px solid #2b2b2b; + border-bottom-width: 3px; + + img { + height: calc(100% + 3px); + } + + .enable__is-dark-mode & { + border: .38px solid #9a6c6c; + border-bottom-width: 3px; + } } - &:hover{ - opacity: 0.4; - border: .1px solid #2b2b2b; - } + &:focus-visible{ margin: 3px; border: .1px solid #2b2b2b; diff --git a/less/load-more.less b/less/load-more.less new file mode 100755 index 00000000..2fa39b8a --- /dev/null +++ b/less/load-more.less @@ -0,0 +1,129 @@ +@import "shared/mixins-and-vars"; + +@button-bg1: #0c3f75; +@button-bg2: #3a4b5c; +@button-txt: #fff; + +#product-grid, +#view-grid { + width: 100%; + box-sizing: border-box; + display: flex; + flex-wrap: wrap; + justify-content: center; +} + +.product-tile, +.view-tile { + position: relative; + width: 33.3333%; + box-sizing: border-box; + display: inline-block; + padding: 20px; + img { + max-width: 100%; + border: 1px solid #ccc; + padding: 20px; + } + @media @mobile { + width: 100%; + } +} + +.product-name { + font-size: (16 / @px); +} + +.product-price { + font-size: (20 / @px); + font-weight: bold; + margin-bottom: 15px; +} + +.product-details-link { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} + +.view-details-link { + position: absolute; + font-size: (16 / @px); + line-height: (44 / @px); + left: 0; + bottom: 0; + font-weight: bold; + width: 100%; + background: @button-bg1; + color: @button-txt; + min-height: 44px; + border: none; + outline-offset: 4px; + text-align: center; + text-decoration: none; + @media @tablet { + font-size: (12 / @px); + line-height: (34 / @px); + min-height: 34px; + } +} + +#product-count, +#category-count { + text-align: center; +} + +#load-more-btn, +#display-more-btn { + font-size: (15 / @px); + font-weight: bold; + min-width: 200px; + min-height: 44px; + display: block; + cursor: pointer; + background: @button-bg2; + color: @button-txt; + border: none; + margin: 10px auto; + outline-offset: 4px; +} + +#product-reset-btn, +#display-reset-btn { + font-size: (15 / @px); + font-weight: bold; + min-width: 200px; + min-height: 44px; + display: block; + cursor: pointer; + background: none; + border: none; + margin: 10px auto; + outline-offset: 4px; + text-decoration: underline; + + .enable__is-dark-mode & { + color: @dark-mode-white; + } +} + +.add-to-cart-btn { + font-size: (15 / @px); + font-weight: bold; + min-height: 44px; + background: @button-bg1; + color: @button-txt; + border: none; + padding: 0 15px; + outline-offset: 4px; +} + +.hide-btn { + display: none !important; +} + +.tile-relative { + position: relative; +} diff --git a/sitemap.txt b/sitemap.txt index 8003cc54..5e113005 100644 --- a/sitemap.txt +++ b/sitemap.txt @@ -27,6 +27,7 @@ https://www.useragentman.com/enable/image-gallery.php https://www.useragentman.com/enable/infographic.php https://www.useragentman.com/enable/input-mask.php https://www.useragentman.com/enable/listbox.php +https://www.useragentman.com/enable/load-more.php https://www.useragentman.com/enable/log.php https://www.useragentman.com/enable/marquee.php https://www.useragentman.com/enable/meter.php diff --git a/templates/data/meta-info.json b/templates/data/meta-info.json index 56e32550..0d553d51 100644 --- a/templates/data/meta-info.json +++ b/templates/data/meta-info.json @@ -360,6 +360,10 @@ "desc": "Why it's preferable to use native HTML5 select controls vs custom controls, and what developers must do if they have to use a custom one.", "isNPM": "true" }, + "load-more.php": { + "title": "Accessilble Load More Buttons", + "desc": "How to approach pagination using load more buttons." + }, "log.php": { "sectionPages": [ { "code-patterns": "ARIA Live Regions" }], "title": "Accessible Log Display: Understanding, Live Example, and Code Walkthrough",