Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
<ul class="ps-sharebuttons__list">
{foreach from=$social_share_links item='social_share_link'}
<li class="{$social_share_link.class}">
<a href="{$social_share_link.url}" class="ps-sharebuttons__link" title="{$social_share_link.label}" target="_blank" rel="noopener noreferrer">
<a href="{$social_share_link.url}" class="ps-sharebuttons__link" target="_blank" rel="noopener noreferrer"
{if $social_share_link.class}
Comment thread
tblivet marked this conversation as resolved.
aria-label="{l s='Share on %social_share_link_label%' sprintf=['%social_share_link_label%' => $social_share_link.class] d='Shop.Theme.Actions'}"
{/if}
>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" fill="currentColor" viewBox="0 0 16 16">
{if $social_share_link.class == 'facebook'}
<path d="M16 8.049c0-4.446-3.582-8.05-8-8.05C3.58 0-.002 3.603-.002 8.05c0 4.017 2.926 7.347 6.75 7.951v-5.625h-2.03V8.05H6.75V6.275c0-2.017 1.195-3.131 3.022-3.131.876 0 1.791.157 1.791.157v1.98h-1.009c-.993 0-1.303.621-1.303 1.258v1.51h2.218l-.354 2.326H9.25V16c3.824-.604 6.75-3.934 6.75-7.951"/>
Expand Down
120 changes: 120 additions & 0 deletions src/js/accessibility/product.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import {Carousel} from 'bootstrap';
import SelectorsMap from '../constants/selectors-map';

export default () => {
const {prestashop, Theme: {events}} = window;

// Product images sync on open/close modal
document.addEventListener('show.bs.modal', (event) => {
const modal = event.target as HTMLElement;

if (!modal.matches(SelectorsMap.product.productImagesModal)) return;

const modalCarouselElement = modal.querySelector(SelectorsMap.product.productImagesModalCarousel);
const mainCarouselElement = document.querySelector(SelectorsMap.product.carousel);

if (!modalCarouselElement || !mainCarouselElement) return;

const modalCarousel = Carousel.getOrCreateInstance(modalCarouselElement as HTMLElement);
const activeIndex = getActiveSlideIndex(mainCarouselElement as HTMLElement);

if (activeIndex !== -1) {
modalCarousel.to(activeIndex);
}
});

document.addEventListener('hide.bs.modal', (event) => {
const modal = event.target as HTMLElement;

if (!modal.matches(SelectorsMap.product.productImagesModal)) return;

const modalCarouselElement = modal.querySelector(SelectorsMap.product.productImagesModalCarousel);
const mainCarouselElement = document.querySelector(SelectorsMap.product.carousel);

if (!modalCarouselElement || !mainCarouselElement) return;

const mainCarousel = Carousel.getOrCreateInstance(mainCarouselElement as HTMLElement);
const activeIndex = getActiveSlideIndex(modalCarouselElement as HTMLElement);

if (activeIndex !== -1) {
mainCarousel.to(activeIndex);
}
});

const getActiveSlideIndex = (carouselEl: HTMLElement): number => {
const items = carouselEl.querySelectorAll('.carousel-item');

return Array.from(items).findIndex((item) => item.classList.contains('active'));
};

// Stores the element to refocus, per context ("main" or "quickview")
const focusMap = new Map<'quickview' | 'main', { id: string }>();

// Save the element before the update
prestashop.on(events.updateProduct, ({ event }: { event: Event }) => {

Check failure on line 59 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

There should be no space before '}'

Check failure on line 59 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

There should be no space after '{'
const target = event?.target as HTMLElement;
if (!target || !target.id) return;

Check failure on line 61 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

Expected blank line before this statement

const isInQuickView = !!target.closest(SelectorsMap.quickviewModal);
const context: 'quickview' | 'main' = isInQuickView ? 'quickview' : 'main';

focusMap.set(context, { id: target.id });

Check failure on line 66 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

There should be no space before '}'

Check failure on line 66 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

There should be no space after '{'
});

// Restore focus after update
prestashop.on(events.updatedProduct, () => {
focusMap.forEach((focusData, context) => {
let container: HTMLElement | null = null;

if (context === 'quickview') {
container = document.querySelector(SelectorsMap.quickviewModal);
} else {
container = document.querySelector(SelectorsMap.product.container);
}

Check failure on line 79 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

Trailing spaces not allowed
if (!container) return;

Check failure on line 81 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

Trailing spaces not allowed
const elementToFocus = container.querySelector<HTMLElement>(`#${focusData.id}`);

if (elementToFocus) {
elementToFocus.focus();

// Emit event to when the focus is restored
prestashop.emit(events.combinationFocusRestored, {
context,
elementId: focusData.id

Check failure on line 90 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

Missing trailing comma
});
}

focusMap.delete(context);
});
});

// Product availability messages announcement
prestashop.on(events.combinationFocusRestored, ({ context }: { context: 'quickview' | 'main' }) => {

Check failure on line 99 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

There should be no space before '}'

Check failure on line 99 in src/js/accessibility/product.ts

View workflow job for this annotation

GitHub Actions / ESLint

There should be no space after '{'
let container: HTMLElement | null = null;

// Get the container of the context
if (context === 'quickview') {
container = document.querySelector(SelectorsMap.quickviewModal);
} else {
container = document.querySelector(SelectorsMap.product.container);
}

if (!container) return;

const productAvailabilityElement = container.querySelector(SelectorsMap.product.productAvailability);

if (!productAvailabilityElement) return;

// Delay to ensure focus announcement finishes first
setTimeout(() => {
productAvailabilityElement.setAttribute('aria-live', 'polite');
}, 250);
});
};
1 change: 1 addition & 0 deletions src/js/constants/events-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@ export default {
updatedProduct: 'updatedProduct',
updateFacets: 'updateFacets',
updatedDeliveryForm: 'updatedDeliveryForm',
combinationFocusRestored: 'combinationFocusRestored',
};
3 changes: 3 additions & 0 deletions src/js/constants/selectors-map.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,10 @@ const selectorsMap = {
carousel: '.js-product-carousel',
miniature: '.js-product-miniature',
thumbnail: '.js-thumb-container',
productImagesModal: '[data-ps-ref="product-images-modal"]',
productImagesModalCarousel: '[data-ps-ref="product-images-modal-carousel"]',
activeThumbail: (id: number): string => `.js-thumb-container:nth-child(${id + 1})`,
productAvailability: '[data-ps-ref="product-availability"]',
},
order: {
returnForm: '.js-order-return-form',
Expand Down
3 changes: 3 additions & 0 deletions src/js/theme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import initDesktopMenu from './modules/ps_mainmenu';
import initFormValidation from './form-validation';
import initCategoryTree from './modules/ps_categorytree';
import initScrollPaddingTop from './helpers/scrollPadding';
import initProductAccessibility from './accessibility/product';

initEmitter();

Expand All @@ -55,6 +56,8 @@ document.addEventListener('DOMContentLoaded', () => {
initCategoryTree();
initScrollPaddingTop();
initBlockCart();
// Accessibility
initProductAccessibility();

prestashop.on(events.responsiveUpdate, () => {
initSearchbar();
Expand Down
11 changes: 9 additions & 2 deletions src/scss/prestashop/pages/_product.scss
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,20 @@ $component-name: product;
}

&__thumbnail {
padding: 0;
background-color: transparent;
border: none;
border-radius: var(--bs-border-radius);

&.active .#{$component-name}__thumbnail-image {
outline: 2px solid var(--bs-primary);
outline: 0.125rem solid var(--bs-primary);
outline-offset: -0.125rem;
}

&-image {
border-radius: var(--bs-border-radius);
outline: 2px solid transparent;
outline: 0.125rem solid transparent;
outline-offset: -0.125rem;
}
}

Expand Down
2 changes: 1 addition & 1 deletion templates/_partials/notifications.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@

{if $notifications.info}
{block name='notifications_info'}
<article class="alert alert-info" role="alert" data-alert="info">
<article class="alert alert-info" role="status" data-alert="info">
<ul class="mb-0">
{foreach $notifications.info as $notif}
<li>{$notif nofilter}</li>
Expand Down
9 changes: 6 additions & 3 deletions templates/catalog/_partials/product-add-to-cart.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@
{/if}

{** And render the availability message with icon *}
<div class="product__availability-status {$availability_class}" role="alert">
<div class="product__availability-status {$availability_class}" aria-live="off" data-ps-ref="product-availability">
<i class="product__availability-icon material-icons rtl-no-flip">&#x{$availability_icon};</i>

<div class="product__availability-messages">
<span class="visually-hidden">{l s='Product availability:' d='Shop.Theme.Global'}</span>
<span>{$product.availability_message}</span>

{if !empty($product.availability_submessage)}
<br>
<small>{$product.availability_submessage}</small>
<small class="d-block">{$product.availability_submessage}</small>
{/if}
</div>
</div>
Expand Down Expand Up @@ -79,9 +79,12 @@
data-button-action="add-to-cart"
type="submit"
{if !$product.add_to_cart_url}
aria-disabled="true"
disabled
{/if}
data-ps-ref="add-to-cart"
aria-label="{l s='Add to cart %product_name%' sprintf=['%product_name%' => $product.name] d='Shop.Theme.Actions'}"
title="{l s='Add to cart %product_name%' sprintf=['%product_name%' => $product.name] d='Shop.Theme.Actions'}"
>
<i class="material-icons" aria-hidden="true">&#xE547;</i>
{l s='Add to cart' d='Shop.Theme.Actions'}
Expand Down
64 changes: 31 additions & 33 deletions templates/catalog/_partials/product-cover-thumbnails.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,13 @@
<div
id="product-images-{$product.id}"
class="product__carousel carousel slide js-product-carousel"
data-bs-ride="carousel"
>
<div class="carousel-inner">
{include file='catalog/_partials/product-flags.tpl'}

{if $product.images|@count > 1}
<button class="carousel-control-prev outline outline--rounded" type="button" data-bs-target="#product-images-{$product.id}" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">{l s='Previous image' d='Shop.Theme.Global'}</span>
</button>

<button class="carousel-control-next outline outline--rounded" type="button" data-bs-target="#product-images-{$product.id}" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">{l s='Next image' d='Shop.Theme.Global'}</span>
</button>
{/if}
{include file='catalog/_partials/product-flags.tpl'}

<div class="carousel-inner">
{block name='product_cover'}
{foreach from=$product.images item=image key=key name=productImages}
<div class="carousel-item{if $image.id_image == $product.default_image.id_image} active{/if}"
data-bs-target="#product-images-modal-{$product.id}"
data-bs-slide-to="{$key}"
>
<div class="carousel-item{if $image.id_image == $product.default_image.id_image} active{/if}">
<picture>
{if isset($image.bySize.default_xl.sources.avif)}
<source
Expand Down Expand Up @@ -67,28 +51,42 @@
data-full-size-image-url="{$image.bySize.home_default.url}"
>
</picture>

<div class="product__zoom btn btn-tertiary btn-square-icon" data-bs-toggle="modal" data-bs-target="#product-modal">
<i class="material-icons">&#xE8B6;</i>
</div>
</div>
{/foreach}
{/block}
</div>

{if $product.images|@count > 1}
<button class="carousel-control-prev outline outline--rounded" type="button" data-bs-target="#product-images-{$product.id}" data-bs-slide="prev">
<span class="carousel-control-prev-icon" aria-hidden="true"></span>
<span class="visually-hidden">{l s='Previous image' d='Shop.Theme.Global'}</span>
</button>

<button class="carousel-control-next outline outline--rounded" type="button" data-bs-target="#product-images-{$product.id}" data-bs-slide="next">
<span class="carousel-control-next-icon" aria-hidden="true"></span>
<span class="visually-hidden">{l s='Next image' d='Shop.Theme.Global'}</span>
</button>
{/if}

{block name='product_images_modal_button'}
<button class="product__zoom btn btn-tertiary outline outline--rounded btn-square-icon" data-bs-toggle="modal" data-bs-target="#product-modal" aria-label="{l s='Open zoomed product image gallery' d='Shop.Theme.Global'}" title="{l s='Open zoomed product image gallery' d='Shop.Theme.Global'}">
<i class="material-icons">&#xE8B6;</i>
</button>
{/block}
</div>

{block name='product_images'}
<div class="product__thumbnails">
<ul class="product__thumbnails-list">
{foreach from=$product.images item=image key=key}
<li
class="product__thumbnail js-thumb-container{if $image.id_image == $product.default_image.id_image} active{/if}"
{foreach from=$product.images item=image key=key name=productThumbnails}
<button
class="product__thumbnail focus-ring js-thumb-container{if $image.id_image == $product.default_image.id_image} active{/if}"
data-bs-target="#product-images-{$product.id}"
data-bs-slide-to="{$key}"
{if $image.id_image == $product.default_image.id_image}
aria-current="true"
{/if}
aria-label="{l s='Product image %number%' d='Shop.Theme.Catalog' sprintf=['%number%' => $key]}"
aria-label="{l s='Slide to product image %number%' d='Shop.Theme.Catalog' sprintf=['%number%' => $key + 1]}"
>
<picture>
{if isset($image.bySize.default_xs.sources.avif)}
Expand All @@ -110,7 +108,7 @@
{/if}

<img
class="product__thumbnail-image img-fluid js-thumb{if $image.id_image == $product.default_image.id_image} js-thumb-selected{/if}"
class="product__thumbnail-image outline outline--rounded img-fluid js-thumb{if $image.id_image == $product.default_image.id_image} js-thumb-selected{/if}"
srcset="
{$image.bySize.default_xs.url},
{$image.bySize.default_xl.url} 2x"
Expand All @@ -121,7 +119,7 @@
title="{$image.legend}"
>
</picture>
</li>
</button>
{/foreach}
</ul>
</div>
Expand Down Expand Up @@ -165,8 +163,8 @@
>
</picture>
{/if}
</div>

{block name='product_images_modal'}
{include file='catalog/_partials/product-images-modal.tpl'}
{/block}
{block name='product_images_modal'}
{include file='catalog/_partials/product-images-modal.tpl'}
{/block}
</div>
12 changes: 6 additions & 6 deletions templates/catalog/_partials/product-customization.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,26 @@
<form method="post" action="{$product.url}" enctype="multipart/form-data" class="mb-0">
{foreach from=$customizations.fields item="field"}
<div class="product-customization__item">
<p class="product-customization__label form-label {if $field.required}required{/if}">{$field.label}</p>
<label class="product-customization__label form-label {if $field.required}required{/if}" for="field-{$field.id_customization_field}">{$field.label}</label>

<div class="product-customization__field">
{if $field.type === 'text'}
<textarea placeholder="{l s='Your message here' d='Shop.Forms.Help'}" class="form-control product-message" maxlength="250" {if $field.required} required {/if} name="{$field.input_name}"></textarea>
<textarea placeholder="{l s='Your message here' d='Shop.Forms.Help'}" class="form-control product-message" maxlength="250" {if $field.required} required {/if} name="{$field.input_name}" id="field-{$field.id_customization_field}"></textarea>

{if $field.text !== ''}
<div class="product-customization__message">
<b>{l s='Your customization:' d='Shop.Theme.Catalog'}</b> {$field.text}
</div>
{/if}
{elseif $field.type === 'image'}
<input class="form-control file-input js-file-input" {if $field.required} required {/if} type="file" name="{$field.input_name}">
<input class="form-control file-input js-file-input" {if $field.required} required {/if} type="file" name="{$field.input_name}" id="field-{$field.id_customization_field}">

{if $field.is_customized}
<div class="product-customization__image-wrapper">
<img src="{$field.image.small.url}" class="product-customization__image img-fluid" loading="lazy">

<a class="product-customization__image-remove link-danger" href="{$field.remove_image_url}" rel="nofollow">
{l s='Remove Image' d='Shop.Theme.Actions'}
<a class="product-customization__image-remove link-danger" href="{$field.remove_image_url}" rel="nofollow" role="button">
{l s='Remove image' d='Shop.Theme.Actions'}
</a>
</div>
{/if}
Expand All @@ -51,7 +51,7 @@
{/foreach}

<div class="product-customization__action">
<button class="btn btn-primary" type="submit" name="submitCustomizedData">{l s='Save Customization' d='Shop.Theme.Actions'}</button>
<button class="btn btn-primary" type="submit" name="submitCustomizedData">{l s='Save customization' d='Shop.Theme.Actions'}</button>
</div>
</form>
{/block}
Expand Down
Loading