diff --git a/CHANGELOG.md b/CHANGELOG.md index c6a1e7b6bc..f471652397 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ # Changelog ## Draft +- Add placeholder for failed to load carousel images and update scalability. [#2009](https://github.com/bigcommerce/cornerstone/pull/2009) - Fixed insufficient button label on cart page from action controls. [#2013](https://github.com/bigcommerce/cornerstone/pull/2013) - "Skip to main content" now is visible when top banned is absent. [#2010](https://github.com/bigcommerce/cornerstone/pull/2010) - Announce subscribing email field as mandatory. [#2011](https://github.com/bigcommerce/cornerstone/pull/2011) diff --git a/assets/img/hero-carousel-image-load-error.svg b/assets/img/hero-carousel-image-load-error.svg new file mode 100644 index 0000000000..a0127ca216 --- /dev/null +++ b/assets/img/hero-carousel-image-load-error.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/assets/js/theme/common/carousel/index.js b/assets/js/theme/common/carousel/index.js index a6b7ef8e1b..1cdc858699 100644 --- a/assets/js/theme/common/carousel/index.js +++ b/assets/js/theme/common/carousel/index.js @@ -1,85 +1,84 @@ import 'slick-carousel'; import { + activatePlayPauseButton, + arrowAriaLabling, dotsSetup, - tooltipSetup, + getActiveSlideIdxAndSlidesQuantity, + handleImageAspectRatio, + handleImageLoad, setTabindexes, - arrowAriaLabling, + tooltipSetup, updateTextWithLiveData, - heroCarouselSetup, - getRealSlidesQuantityAndCurrentSlide, } from './utils'; /** - * returns actualSlide and actualSlideCount + * returns activeSlideIdx and slidesQuantity * based on provided carousel settings * @param {Object} $slickSettings * @returns {Object} */ const extractSlidesDetails = ({ - slideCount, currentSlide, breakpointSettings, activeBreakpoint, $slider, -}) => getRealSlidesQuantityAndCurrentSlide( - breakpointSettings, - activeBreakpoint, - currentSlide, + slideCount, $slides, options: { slidesToShow, slidesToScroll }, +}) => getActiveSlideIdxAndSlidesQuantity( slideCount, - $slider.data('slick').slidesToScroll, + slidesToShow, + slidesToScroll, + $slides, ); -export const onCarouselClick = ({ - data: $activeSlider, -}) => { +export const onUserCarouselChange = ({ data }, context, $slider) => { + const $activeSlider = $slider || data; const $parentContainer = $activeSlider.hasClass('productView-thumbnails') ? $('.productView-images') : $activeSlider; - const { actualSlideCount, actualSlide } = extractSlidesDetails($activeSlider[0].slick); - const $carouselContentElement = $('.js-carousel-content-change-message', $parentContainer); - const carouselContentInitText = $carouselContentElement.text(); - const carouselContentAnnounceMessage = updateTextWithLiveData(carouselContentInitText, (actualSlide + 1), actualSlideCount); + const { activeSlideIdx, slidesQuantity } = extractSlidesDetails($activeSlider[0].slick); + const $carouselContentElement = $('[data-carousel-content-change-message]', $parentContainer); + const carouselContentAnnounceMessage = updateTextWithLiveData(context.carouselContentAnnounceMessage, (activeSlideIdx + 1), slidesQuantity); $carouselContentElement.text(carouselContentAnnounceMessage); }; -export const onCarouselChange = (event, carousel) => { +export const onSlickCarouselChange = (e, carousel, context) => { const { - options: { prevArrow, nextArrow }, - $prevArrow, - $nextArrow, $dots, $slider, + $prevArrow, + $nextArrow, } = carousel; - const { actualSlideCount, actualSlide } = extractSlidesDetails(carousel); - const $prevArrowNode = $prevArrow || $slider.find(prevArrow); - const $nextArrowNode = $nextArrow || $slider.find(nextArrow); + const { activeSlideIdx, slidesQuantity } = extractSlidesDetails(carousel); - const dataArrowLabel = $slider.data('arrow-label'); - - if (dataArrowLabel) { - $prevArrowNode.attr('aria-label', dataArrowLabel); - $nextArrowNode.attr('aria-label', dataArrowLabel); - $slider.data('arrow-label', false); - } - - dotsSetup($dots, actualSlide, actualSlideCount, $slider.data('dots-labels')); - setTabindexes($slider.find('.slick-slide'), $prevArrowNode, $nextArrowNode, actualSlide, actualSlideCount); - arrowAriaLabling($prevArrowNode, $nextArrowNode, actualSlide, actualSlideCount); - tooltipSetup($prevArrowNode, $nextArrowNode, $dots); + dotsSetup($dots, activeSlideIdx, slidesQuantity, context); + arrowAriaLabling($prevArrow, $nextArrow, activeSlideIdx, slidesQuantity, context.carouselArrowAndDotAriaLabel); + setTabindexes($slider.find('.slick-slide')); + tooltipSetup($prevArrow, $nextArrow, $dots); + activatePlayPauseButton(carousel, slidesQuantity, context); }; -export default function () { - const $carouselCollection = $('[data-slick]'); - - if ($carouselCollection.length === 0) return; - - $carouselCollection.each((index, carousel) => { +export default function (context) { + $('[data-slick]').each((idx, carousel) => { // getting element using find to pass jest test const $carousel = $(document).find(carousel); - $carousel.on('init afterChange', onCarouselChange); - $carousel.on('click', '.slick-arrow, .js-carousel-dot', $carousel, onCarouselClick); + $carousel.on('init afterChange', (e, carouselObj) => onSlickCarouselChange(e, carouselObj, context)); + $carousel.on('click', '.slick-arrow, .slick-dots', $carousel, e => onUserCarouselChange(e, context)); + $carousel.on('swipe', (e, carouselObj) => onUserCarouselChange(e, context, carouselObj.$slider)); + + if ($carousel.hasClass('heroCarousel')) { + $carousel.on('init afterChange', handleImageLoad); + $carousel.on('swipe', handleImageAspectRatio); + $carousel.on('click', '.slick-arrow, .slick-dots', $carousel, handleImageAspectRatio); + + // Alternative image styling for IE, which doesn't support objectfit + if (typeof document.documentElement.style.objectFit === 'undefined') { + $carousel.find('.heroCarousel-slide').each((index, slide) => { + $(slide).addClass('compat-object-fit'); + }); + } + } const isMultipleSlides = $carousel.children().length > 1; const customPaging = isMultipleSlides ? () => ( - '' + '' ) : () => {}; @@ -90,6 +89,4 @@ export default function () { dots: isMultipleSlides, }); }); - - heroCarouselSetup($carouselCollection.filter('.heroCarousel')); } diff --git a/assets/js/theme/common/carousel/utils/activatePlayPauseButton.js b/assets/js/theme/common/carousel/utils/activatePlayPauseButton.js new file mode 100644 index 0000000000..7405369771 --- /dev/null +++ b/assets/js/theme/common/carousel/utils/activatePlayPauseButton.js @@ -0,0 +1,50 @@ +import { throttle } from 'lodash'; + +const PLAY_ACTION = 'slickPlay'; +const PAUSE_ACTION = 'slickPause'; +const IS_ACTIVATED_DATA_ATTR = 'is-activated'; + +export default (carousel, slidesQuantity, context) => { + const { $slider, $dots, speed } = carousel; + const $playPauseButton = $slider.find('[data-play-pause-button]'); + + if ($playPauseButton.length === 0) return; + + $playPauseButton.css('display', slidesQuantity < 2 ? 'none' : 'block'); + + if ($playPauseButton.data(IS_ACTIVATED_DATA_ATTR)) return; + + const { + carouselPlayPauseButtonPlay, + carouselPlayPauseButtonPause, + carouselPlayPauseButtonAriaPlay, + carouselPlayPauseButtonAriaPause, + } = context; + + const updateLabels = action => { + $playPauseButton + .text(action === PLAY_ACTION + ? carouselPlayPauseButtonPause : carouselPlayPauseButtonPlay) + .attr('aria-label', action === PLAY_ACTION + ? carouselPlayPauseButtonAriaPause : carouselPlayPauseButtonAriaPlay); + }; + + const onPlayPauseClick = () => { + const action = carousel.paused ? PLAY_ACTION : PAUSE_ACTION; + + $slider.slick(action); + updateLabels(action); + }; + + // for correct carousel controls focus order + if ($dots) { + $playPauseButton.insertBefore($dots); + } else $slider.append($playPauseButton); + + $playPauseButton.on('click', throttle(onPlayPauseClick, speed, { trailing: false })); + $playPauseButton.data(IS_ACTIVATED_DATA_ATTR, true); + + if (carousel.breakpoints.length) { + $slider.on('breakpoint', () => updateLabels(PLAY_ACTION)); + } +}; diff --git a/assets/js/theme/common/carousel/utils/arrowAriaLabling.js b/assets/js/theme/common/carousel/utils/arrowAriaLabling.js index 8ae8982471..1751262d1d 100644 --- a/assets/js/theme/common/carousel/utils/arrowAriaLabling.js +++ b/assets/js/theme/common/carousel/utils/arrowAriaLabling.js @@ -1,19 +1,17 @@ import updateTextWithLiveData from './updateTextWithLiveData'; -export default ($prevArrow, $nextArrow, actualSlide, actualSlideCount) => { - if (actualSlideCount < 2) return; - if ($prevArrow.length === 0 || $nextArrow.length === 0) return; +export default ($prevArrow, $nextArrow, activeSlideIdx, slidesQuantity, ariaLabel) => { + if (slidesQuantity < 2 || !$prevArrow || !$nextArrow) return; - const arrowAriaLabelBaseText = $prevArrow.attr('aria-label'); - const currentSlideNumber = actualSlide + 1; + const activeSlideNumber = activeSlideIdx + 1; - const prevSlideNumber = actualSlide === 0 ? actualSlideCount : currentSlideNumber - 1; - const arrowLeftText = updateTextWithLiveData(arrowAriaLabelBaseText, prevSlideNumber, actualSlideCount); + const prevSlideNumber = activeSlideIdx === 0 ? slidesQuantity : activeSlideNumber - 1; + const arrowLeftText = updateTextWithLiveData(ariaLabel, prevSlideNumber, slidesQuantity); $prevArrow.attr('aria-label', arrowLeftText); - const nextSlideNumber = actualSlide === actualSlideCount - 1 ? 1 : currentSlideNumber + 1; - const arrowRightText = updateTextWithLiveData(arrowAriaLabelBaseText, nextSlideNumber, actualSlideCount); + const nextSlideNumber = activeSlideIdx === slidesQuantity - 1 ? 1 : activeSlideNumber + 1; + const arrowRightText = updateTextWithLiveData(ariaLabel, nextSlideNumber, slidesQuantity); $nextArrow.attr('aria-label', arrowRightText); }; diff --git a/assets/js/theme/common/carousel/utils/dotsSetup.js b/assets/js/theme/common/carousel/utils/dotsSetup.js index c6dff39049..9db5927d7d 100644 --- a/assets/js/theme/common/carousel/utils/dotsSetup.js +++ b/assets/js/theme/common/carousel/utils/dotsSetup.js @@ -1,21 +1,20 @@ -export default ($dots, actualSlide, actualSlideCount, dotLabels) => { +import updateTextWithLiveData from './updateTextWithLiveData'; + +export default ($dots, activeSlideIdx, slidesQuantity, { carouselArrowAndDotAriaLabel, carouselActiveDotAriaLabel }) => { if (!$dots) return; - if (actualSlideCount === 1) { + if (slidesQuantity < 2) { $dots.css('display', 'none'); return; } $dots.css('display', 'block'); - const { dotAriaLabel, activeDotAriaLabel } = dotLabels; + $dots.children().each((idx, dot) => { + const dotLabelText = updateTextWithLiveData(carouselArrowAndDotAriaLabel, idx + 1, slidesQuantity); + const dotSlideStatusText = idx === activeSlideIdx ? `, ${carouselActiveDotAriaLabel}` : ''; + const dotAriaLabel = `${dotLabelText}${dotSlideStatusText}`; - $dots.children().each((index, dot) => { - const $dot = $(dot); - const dotSlideNumber = index + 1; - const dotAriaLabelComputed = index === actualSlide - ? `${dotAriaLabel} ${dotSlideNumber}, ${activeDotAriaLabel}` - : `${dotAriaLabel} ${dotSlideNumber}`; - $dot.find('button').attr('aria-label', dotAriaLabelComputed); + $(dot).find('[data-carousel-dot]').attr('aria-label', dotAriaLabel); }); }; diff --git a/assets/js/theme/common/carousel/utils/getActiveSlideIdxAndSlidesQuantity.js b/assets/js/theme/common/carousel/utils/getActiveSlideIdxAndSlidesQuantity.js new file mode 100644 index 0000000000..352a82dc80 --- /dev/null +++ b/assets/js/theme/common/carousel/utils/getActiveSlideIdxAndSlidesQuantity.js @@ -0,0 +1,23 @@ +export default (slideCount, slidesToShow, slidesToScroll, $slides) => { + const lastVisibleIdx = $slides.get().reduce((acc, curr, idx) => { + if ($(curr).hasClass('slick-active')) return idx; + return acc; + }, -1); + + const activeSlideIdx = lastVisibleIdx < slidesToShow + ? 0 + : Math.ceil((lastVisibleIdx + 1 - slidesToShow) / slidesToScroll); + + let slidesQuantity; + if (slideCount === 0) { + slidesQuantity = 0; + } else if (slideCount <= slidesToShow) { + slidesQuantity = 1; + } else slidesQuantity = Math.ceil((slideCount - slidesToShow) / slidesToScroll) + 1; + + // FYI - one slide can contain several card items for product carousel + return { + activeSlideIdx, + slidesQuantity, + }; +}; diff --git a/assets/js/theme/common/carousel/utils/getActiveSlideInfo.js b/assets/js/theme/common/carousel/utils/getActiveSlideInfo.js new file mode 100644 index 0000000000..c5b67c770e --- /dev/null +++ b/assets/js/theme/common/carousel/utils/getActiveSlideInfo.js @@ -0,0 +1,20 @@ +export default ({ $slider }, isAnalyzedDataAttr) => { + const $activeSlide = $slider.find('.slick-current'); + const isAnalyzedSlide = $activeSlide.data(isAnalyzedDataAttr); + + if (isAnalyzedSlide) return { isAnalyzedSlide }; + + const $activeSlideImg = $activeSlide.find('.heroCarousel-image'); + + const attrsObj = { + src: $activeSlideImg.attr('src'), + srcset: $activeSlideImg.attr('srcset'), + sizes: $activeSlideImg.attr('sizes'), + }; + + return { + attrsObj, + $slider, + $activeSlide, + }; +}; diff --git a/assets/js/theme/common/carousel/utils/getRealSlidesQuantityAndCurrentSlide.js b/assets/js/theme/common/carousel/utils/getRealSlidesQuantityAndCurrentSlide.js deleted file mode 100644 index 74eacffdd2..0000000000 --- a/assets/js/theme/common/carousel/utils/getRealSlidesQuantityAndCurrentSlide.js +++ /dev/null @@ -1,17 +0,0 @@ -export default ( - breakpointSettings, - activeBreakpoint, - currentSlide, - slideCount, - defaultSlidesToScrollQuantity = 1, -) => { - const slidesToScrollQuantity = activeBreakpoint - /* eslint-disable dot-notation */ - ? breakpointSettings[activeBreakpoint]['slidesToScroll'] - : defaultSlidesToScrollQuantity; - - return { - actualSlideCount: Math.ceil(slideCount / slidesToScrollQuantity), - actualSlide: Math.ceil(currentSlide / slidesToScrollQuantity), - }; -}; diff --git a/assets/js/theme/common/carousel/utils/handleImageAspectRatio.js b/assets/js/theme/common/carousel/utils/handleImageAspectRatio.js new file mode 100644 index 0000000000..ef163e433e --- /dev/null +++ b/assets/js/theme/common/carousel/utils/handleImageAspectRatio.js @@ -0,0 +1,46 @@ +import getActiveSlideInfo from './getActiveSlideInfo'; + +const IMAGE_CLASSES = { + vertical: 'is-vertical-image-type', + square: 'is-square-image-type', +}; +const IS_ANALYZED_DATA_ATTR = 'image-ratio-analyzed'; + +const defineClass = (imageAspectRatio) => { + switch (true) { + case imageAspectRatio > 0.8 && imageAspectRatio <= 1.2: + return IMAGE_CLASSES.square; + case imageAspectRatio > 1.2: + return IMAGE_CLASSES.vertical; + default: + return ''; + } +}; + +export default ({ delegateTarget }, carousel) => { + const { + isAnalyzedSlide, + attrsObj, + $slider, + $activeSlide, + } = getActiveSlideInfo(carousel || delegateTarget.slick, IS_ANALYZED_DATA_ATTR); + + if (isAnalyzedSlide) return; + + const $activeSlideAndClones = $slider.find(`[data-hero-slide=${$activeSlide.data('hero-slide')}]`); + $activeSlideAndClones.each((idx, slide) => $(slide).data(IS_ANALYZED_DATA_ATTR, true)); + + if ($activeSlide.find('.heroCarousel-content').length) return; + + $('') + .on('load', function getImageSizes() { + const imageHeight = this.height; + const imageWidth = this.width; + + if (imageHeight < 2 || imageWidth < 2) return; + + const imageAspectRatio = imageHeight / imageWidth; + $activeSlideAndClones.each((idx, slide) => $(slide).addClass(defineClass(imageAspectRatio))); + }) + .attr(attrsObj); +}; diff --git a/assets/js/theme/common/carousel/utils/handleImageLoad.js b/assets/js/theme/common/carousel/utils/handleImageLoad.js new file mode 100644 index 0000000000..66572db108 --- /dev/null +++ b/assets/js/theme/common/carousel/utils/handleImageLoad.js @@ -0,0 +1,20 @@ +import getActiveSlideInfo from './getActiveSlideInfo'; + +const IMAGE_ERROR_CLASS = 'is-image-error'; +const IS_ANALYZED_DATA_ATTR = 'image-load-analyzed'; + +export default (e, carousel) => { + const { + isAnalyzedSlide, + attrsObj, + $activeSlide, + } = getActiveSlideInfo(carousel, IS_ANALYZED_DATA_ATTR); + + if (isAnalyzedSlide) return; + + $activeSlide.data(IS_ANALYZED_DATA_ATTR, true); + + $('') + .on('error', () => $activeSlide.addClass(IMAGE_ERROR_CLASS)) + .attr(attrsObj); +}; diff --git a/assets/js/theme/common/carousel/utils/heroCarouselSetup.js b/assets/js/theme/common/carousel/utils/heroCarouselSetup.js deleted file mode 100644 index e7aa880538..0000000000 --- a/assets/js/theme/common/carousel/utils/heroCarouselSetup.js +++ /dev/null @@ -1,63 +0,0 @@ -import playPause from './playPause'; - -const showCarouselIfSlidesAnalyzedSetup = ($carousel) => { - const analyzedSlides = []; - return ($slides) => ($slide) => { - analyzedSlides.push($slide); - if ($slides.length === analyzedSlides.length) { - $carousel.addClass('is-visible'); - } - }; -}; - -export default ($heroCarousel) => { - if ($heroCarousel.length === 0) return; - - playPause($heroCarousel); - - const $slidesNodes = $heroCarousel.find('.heroCarousel-slide'); - const showCarouselIfSlidesAnalyzed = showCarouselIfSlidesAnalyzedSetup($heroCarousel)($slidesNodes); - - $slidesNodes.each((index, element) => { - const $element = $(element); - const isContentBlock = !!$element.find('.heroCarousel-content').length; - - if (isContentBlock) { - showCarouselIfSlidesAnalyzed($element); - return true; - } - - const $image = $element.find('.heroCarousel-image-wrapper img'); - $('') - .attr('src', $($image).attr('src')) - .load(function getImageSizes() { - const imageRealWidth = this.width; - const imageRealHeight = this.height; - - const imageAspectRatio = imageRealHeight / imageRealWidth; - - $element.addClass(() => { - switch (true) { - case imageAspectRatio > 0.8 && imageAspectRatio <= 1.2: - return 'is-square-image-type'; - case imageAspectRatio > 1.2: - return 'is-vertical-image-type'; - default: - return ''; - } - }); - - showCarouselIfSlidesAnalyzed($element); - }) - .error(() => { - showCarouselIfSlidesAnalyzed($element); - }); - }); - - // Alternative image styling for IE, which doesn't support objectfit - if (document.documentElement.style.objectFit === undefined) { - $slidesNodes.each((index, element) => { - $(element).addClass('compat-object-fit'); - }); - } -}; diff --git a/assets/js/theme/common/carousel/utils/index.js b/assets/js/theme/common/carousel/utils/index.js index 4c15ee62f4..3e7e2dd0a0 100644 --- a/assets/js/theme/common/carousel/utils/index.js +++ b/assets/js/theme/common/carousel/utils/index.js @@ -1,7 +1,9 @@ -export { default as heroCarouselSetup } from './heroCarouselSetup'; +export { default as activatePlayPauseButton } from './activatePlayPauseButton'; export { default as arrowAriaLabling } from './arrowAriaLabling'; -export { default as updateTextWithLiveData } from './updateTextWithLiveData'; export { default as dotsSetup } from './dotsSetup'; -export { default as getRealSlidesQuantityAndCurrentSlide } from './getRealSlidesQuantityAndCurrentSlide'; +export { default as getActiveSlideIdxAndSlidesQuantity } from './getActiveSlideIdxAndSlidesQuantity'; +export { default as handleImageAspectRatio } from './handleImageAspectRatio'; +export { default as handleImageLoad } from './handleImageLoad'; export { default as setTabindexes } from './setTabindexes'; export { default as tooltipSetup } from './tooltipSetup'; +export { default as updateTextWithLiveData } from './updateTextWithLiveData'; diff --git a/assets/js/theme/common/carousel/utils/playPause.js b/assets/js/theme/common/carousel/utils/playPause.js deleted file mode 100644 index 7fc67b55dd..0000000000 --- a/assets/js/theme/common/carousel/utils/playPause.js +++ /dev/null @@ -1,38 +0,0 @@ -import { throttle } from 'lodash'; - -const playAction = 'slickPlay'; -const pauseAction = 'slickPause'; - -export default ($heroCarousel) => { - const $playPauseButton = $('.js-hero-play-pause-button'); - - if ($playPauseButton.length === 0) return; - - const slickSettings = $heroCarousel[0].slick; - if (!slickSettings) return; - - const { slideCount, options: { speed } } = slickSettings; - if (slideCount < 2) { - $playPauseButton.css('display', 'none'); - return; - } - - const onPlayPauseClick = () => { - const isCarouselPlaying = $playPauseButton.data('play'); - const action = isCarouselPlaying ? pauseAction : playAction; - const { - play, - ariaPlay, - pause, - ariaPause, - } = $playPauseButton.data('labels'); - - $heroCarousel.slick(action); - $playPauseButton - .data('play', !isCarouselPlaying) - .text(action === playAction ? pause : play) - .attr('aria-label', action === playAction ? ariaPause : ariaPlay); - }; - - $playPauseButton.on('click', throttle(onPlayPauseClick, speed, { trailing: false })); -}; diff --git a/assets/js/theme/common/carousel/utils/setTabindexes.js b/assets/js/theme/common/carousel/utils/setTabindexes.js index 0fe2b1e103..892b2ca0c4 100644 --- a/assets/js/theme/common/carousel/utils/setTabindexes.js +++ b/assets/js/theme/common/carousel/utils/setTabindexes.js @@ -1,22 +1,14 @@ -const allFocusableElementsSelector = '[href], button, input, textarea, select, details, [contenteditable="true"], [tabindex]'; +const FOCUSABLE_ELEMENTS_SELECTOR = '[href], button, input, textarea, select, details, [contenteditable="true"], [tabindex]'; -export default ($slides, $prevArrow, $nextArrow, actualSlide, actualSlideCount) => { - $slides.each((index, element) => { - const $element = $(element); - const tabIndex = $element.hasClass('slick-active') ? 0 : -1; - if ($element.attr('href') !== undefined) { - $element.attr('tabindex', tabIndex); - } +export default ($slides) => { + $slides.each((idx, slide) => { + const $slide = $(slide); + const tabIndex = $slide.hasClass('slick-active') ? 0 : -1; - $element.find(allFocusableElementsSelector).each((idx, child) => { + if ($slide.is(FOCUSABLE_ELEMENTS_SELECTOR)) $slide.attr('tabindex', tabIndex); + + $slide.find(FOCUSABLE_ELEMENTS_SELECTOR).each((index, child) => { $(child).attr('tabindex', tabIndex); }); }); - - if ($prevArrow.length === 0 - || $nextArrow.length === 0 - || $prevArrow.hasClass('js-hero-prev-arrow')) return; - - $prevArrow.attr('aria-disabled', actualSlide === 0); - $nextArrow.attr('aria-disabled', actualSlide === actualSlideCount - 1); }; diff --git a/assets/js/theme/common/carousel/utils/tooltipSetup.js b/assets/js/theme/common/carousel/utils/tooltipSetup.js index acb1bf4526..da4af37856 100644 --- a/assets/js/theme/common/carousel/utils/tooltipSetup.js +++ b/assets/js/theme/common/carousel/utils/tooltipSetup.js @@ -1,12 +1,13 @@ -const carouselTooltipClass = 'carousel-tooltip'; -const carouselTooltip = ``; -const setupTooltipAriaLabel = ($node) => { - const $existedTooltip = $node.find(`.${carouselTooltipClass}`); +const TOOLTIP_DATA_SELECTOR = 'data-carousel-tooltip'; +const TOOLTIP_CLASS = 'carousel-tooltip'; +const TOOLTIP_NODE = ``; +const setupTooltipAriaLabel = ($node) => { + const $existedTooltip = $node.find(`[${TOOLTIP_DATA_SELECTOR}]`); if ($existedTooltip.length) { $existedTooltip.attr('aria-label', $node.attr('aria-label')); } else { - const $tooltip = $(carouselTooltip).attr('aria-label', $node.attr('aria-label')); + const $tooltip = $(TOOLTIP_NODE).attr('aria-label', $node.attr('aria-label')); $node.append($tooltip); } }; @@ -16,13 +17,14 @@ const setupArrowTooltips = (...arrowNodes) => { }; const setupDotTooltips = ($dots) => { - $dots.children().each((idx, dot) => setupTooltipAriaLabel($('.js-carousel-dot', dot))); + $dots.children().each((idx, dot) => setupTooltipAriaLabel($('[data-carousel-dot]', dot))); }; export default ($prevArrow, $nextArrow, $dots) => { - if ($prevArrow.length && $nextArrow.length) { + if ($prevArrow && $nextArrow) { setupArrowTooltips($prevArrow, $nextArrow); } + if ($dots) { setupDotTooltips($dots); } diff --git a/assets/js/theme/common/carousel/utils/updateTextWithLiveData.js b/assets/js/theme/common/carousel/utils/updateTextWithLiveData.js index facff645a6..70b1b70371 100644 --- a/assets/js/theme/common/carousel/utils/updateTextWithLiveData.js +++ b/assets/js/theme/common/carousel/utils/updateTextWithLiveData.js @@ -1,11 +1,8 @@ -const NUMBER = '[NUMBER]'; -const integerRegExp = /[0-9]+/; -const lastIntegerRegExp = /(\d+)(?!.*\d)/; +const SLIDE_NUMBER = '[SLIDE_NUMBER]'; +const SLIDES_QUANTITY = '[SLIDES_QUANTITY]'; -export default (textForChange, slideNumber, slideCount) => { - const valueToReplace = textForChange.includes(NUMBER) ? NUMBER : integerRegExp; - - return textForChange - .replace(valueToReplace, slideNumber) - .replace(lastIntegerRegExp, slideCount); -}; +export default (textForChange, slideNumber, slidesQuantity) => ( + textForChange + .replace(SLIDE_NUMBER, slideNumber) + .replace(SLIDES_QUANTITY, slidesQuantity) +); diff --git a/assets/js/theme/global.js b/assets/js/theme/global.js index 09b9ff6c01..b030eae56e 100644 --- a/assets/js/theme/global.js +++ b/assets/js/theme/global.js @@ -26,7 +26,7 @@ export default class Global extends PageManager { currencySelector(cartId); foundation($(document)); quickView(this.context); - carousel(); + carousel(this.context); menu(); mobileMenuToggle(); privacyCookieNotification(); diff --git a/assets/js/theme/global/quick-view.js b/assets/js/theme/global/quick-view.js index 8acdcfc566..85b13bcdd7 100644 --- a/assets/js/theme/global/quick-view.js +++ b/assets/js/theme/global/quick-view.js @@ -4,7 +4,7 @@ import utils from '@bigcommerce/stencil-utils'; import ProductDetails from '../common/product-details'; import { defaultModal } from './modal'; import 'slick-carousel'; -import { onCarouselChange, onCarouselClick } from '../common/carousel'; +import { onSlickCarouselChange, onUserCarouselChange } from '../common/carousel'; export default function (context) { const modal = defaultModal(); @@ -24,8 +24,9 @@ export default function (context) { const $carousel = modal.$content.find('[data-slick]'); if ($carousel.length) { - $carousel.on('init afterChange', onCarouselChange); - $carousel.on('click', '.slick-arrow', $carousel, onCarouselClick); + $carousel.on('init afterChange', (e, carousel) => onSlickCarouselChange(e, carousel, context)); + $carousel.on('click', '.slick-arrow, .slick-dots', $carousel, e => onUserCarouselChange(e, context)); + $carousel.on('swipe', (e, carouselObj) => onUserCarouselChange(e, context, carouselObj.$slider)); $carousel.slick(); } diff --git a/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss b/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss index 69bbd67094..c109d870eb 100644 --- a/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss +++ b/assets/scss/components/stencil/heroCarousel/_heroCarousel.scss @@ -18,11 +18,6 @@ min-width: 100%; margin-bottom: (spacing("double") + spacing("single")); margin-top: -(spacing("single")); // 3 - opacity: 0; - - &.is-visible { - opacity: 1; - } @include breakpoint("medium") { margin-top: -(spacing("single") + spacing("base")); // 3 @@ -80,6 +75,34 @@ @include carouselOpaqueBackgrounds($slick-dot-bgColor); } } + + .slick-slide { + &.is-square-image-type { + .heroCarousel-image-wrapper { + height: 100vw; + } + } + + &.is-vertical-image-type { + .heroCarousel-image-wrapper { + height: 110vw; + } + } + + &.is-square-image-type, + &.is-vertical-image-type { + .heroCarousel-image-wrapper { + @include breakpoint("small") { + height: 56.25vw; + } + } + } + + &.is-image-error { + background: url("../img/hero-carousel-image-load-error.svg") center center no-repeat; + background-size: contain; + } + } } .heroCarousel-slide { @@ -127,6 +150,7 @@ align-items: flex-start; height: 56.25vw; max-height: 100vh; + transition: height .3s ease; @include breakpoint("small") { max-height: remCalc(400px); @@ -136,34 +160,6 @@ max-height: remCalc(600px); } } - - &.is-square-image-type { - .heroCarousel-image-wrapper { - height: 100vw; - } - } - - &.is-vertical-image-type { - .heroCarousel-image-wrapper { - height: 110vw; - } - } - - &.is-square-image-type, - &.is-vertical-image-type { - .heroCarousel-image-wrapper { - max-height: 100vh; - - @include breakpoint("small") { - height: 56.25vw; - max-height: remCalc(400px); - } - - @include breakpoint("medium") { - max-height: remCalc(600px); - } - } - } } .heroCarousel-content { @@ -223,42 +219,3 @@ margin-top: spacing("single"); } } - -.heroCarousel-play-pause-button { - position: absolute; - left: 15px; - bottom: spacing("third"); - height: 32px; - min-width: 60px; - max-width: 60px; - font-size: 14px; - line-height: 1.25; - font-weight: 700; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - color: $slick-play-pause-button-color; - transition: color 100ms ease-out; - z-index: zIndex("lowest"); - border: 1px solid $slick-play-pause-button-borderColor; - @include carouselOpaqueBackgrounds($slick-play-pause-button-bgColor); - - @media (min-width: 375px) { - min-width: 80px; - max-width: 90px; - } - - @include breakpoint("small") { - max-width: 150px; - font-size: 18px; - } - - @include breakpoint("medium") { - left: 25px; - bottom: spacing("single"); - } - - &:hover { - color: $slick-play-pause-button-color-hover; - } -} diff --git a/assets/scss/components/vendor/slick/_slick.scss b/assets/scss/components/vendor/slick/_slick.scss index 12b15c557e..0896669aba 100644 --- a/assets/scss/components/vendor/slick/_slick.scss +++ b/assets/scss/components/vendor/slick/_slick.scss @@ -187,11 +187,15 @@ div.slick-slider { .carousel-tooltip { height: 1px; - display: block; + display: none; position: relative; margin-top: 10px; @include addFocusTooltip($attr: aria-label); - + + @include breakpoint("small") { + display: block; + } + &:after { padding: 15px 10px; top: 10px; @@ -244,3 +248,47 @@ div.slick-slider { } } } + +// +// Carousel Play/Pause button +// ----------------------------------------------------------------------------- + +.carousel-play-pause-button { + display: none; + position: absolute; + left: 15px; + bottom: spacing("third"); + height: 32px; + min-width: 60px; + max-width: 60px; + font-size: 14px; + line-height: 1.25; + font-weight: 700; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: $slick-play-pause-button-color; + transition: color 100ms ease-out; + z-index: zIndex("lowest"); + border: 1px solid $slick-play-pause-button-borderColor; + @include carouselOpaqueBackgrounds($slick-play-pause-button-bgColor); + + @media (min-width: 375px) { + min-width: 80px; + max-width: 90px; + } + + @include breakpoint("small") { + max-width: 150px; + font-size: 18px; + } + + @include breakpoint("medium") { + left: 25px; + bottom: spacing("single"); + } + + &:hover { + color: $slick-play-pause-button-color-hover; + } +} diff --git a/lang/en.json b/lang/en.json index fd0bda3921..da7cb9e6ce 100755 --- a/lang/en.json +++ b/lang/en.json @@ -146,7 +146,6 @@ "required": "Required", "optional": "Optional", "email_address": "Email Address", - "carousel_slide_number": "Slide number {slide_number}", "edit": "Edit", "not_applicable": "N/A", "no": "No", @@ -902,14 +901,14 @@ "page_builder_link": "Design this page in Page Builder" }, "carousel": { - "arrowAriaLabel": "Go to slide [NUMBER] of", - "contentAnnounceMessage": "You are currently on slide [NUMBER] of", - "dotAriaLabel": "Slide number", - "activeDotAriaLabel": "active", - "playPauseButtonPlay": "Play", - "playPauseButtonPause": "Pause", - "playPauseButtonAriaPlay": "Play carousel", - "playPauseButtonAriaPause": "Pause carousel" + "arrow_and_dot_aria_label": "Go to slide [SLIDE_NUMBER] of [SLIDES_QUANTITY]", + "active_dot_aria_label": "active", + "content_announce_message": "You are currently on slide [SLIDE_NUMBER] of [SLIDES_QUANTITY]", + "play_pause_button_play": "Play", + "play_pause_button_pause": "Pause", + "play_pause_button_aria_play": "Play carousel", + "play_pause_button_aria_pause": "Pause carousel", + "slide_number": "Slide number {slide_number}" }, "validation_messages": { "valid_email": "You must enter a valid email.", diff --git a/templates/components/carousel-content-announcement.html b/templates/components/carousel-content-announcement.html index e3e0eab7bb..b121e5f32a 100644 --- a/templates/components/carousel-content-announcement.html +++ b/templates/components/carousel-content-announcement.html @@ -1,6 +1,5 @@ - - {{lang 'carousel.contentAnnounceMessage'}} {{slides_length}} - + role="status"> diff --git a/templates/components/carousel-content.html b/templates/components/carousel-content.html index 4766512efa..b075caf75a 100644 --- a/templates/components/carousel-content.html +++ b/templates/components/carousel-content.html @@ -2,6 +2,6 @@ {{{heading}}}

{{{text}}}

{{#if button_text}} - {{button_text}} + {{button_text}} {{/if}} diff --git a/templates/components/carousel-play-pause-button.html b/templates/components/carousel-play-pause-button.html new file mode 100644 index 0000000000..0a08b30a2e --- /dev/null +++ b/templates/components/carousel-play-pause-button.html @@ -0,0 +1,13 @@ +{{inject 'carouselPlayPauseButtonPlay' (lang 'carousel.play_pause_button_play')}} +{{inject 'carouselPlayPauseButtonPause' (lang 'carousel.play_pause_button_pause')}} +{{inject 'carouselPlayPauseButtonAriaPlay' (lang 'carousel.play_pause_button_aria_play')}} +{{inject 'carouselPlayPauseButtonAriaPause' (lang 'carousel.play_pause_button_aria_pause')}} + + diff --git a/templates/components/carousel.html b/templates/components/carousel.html index b580b4fce5..b89e1bf5d5 100644 --- a/templates/components/carousel.html +++ b/templates/components/carousel.html @@ -6,23 +6,14 @@ "slidesToScroll": 1, "autoplay": true, "autoplaySpeed": {{carousel.swap_frequency}}, - "lazyLoad": "anticipated", - "slide": ".js-hero-slide", - "prevArrow": ".js-hero-prev-arrow", - "nextArrow": ".js-hero-next-arrow" + "slide": "[data-hero-slide]" }' - data-dots-labels='{ - "dotAriaLabel": "{{lang 'carousel.dotAriaLabel'}}", - "activeDotAriaLabel": "{{lang 'carousel.activeDotAriaLabel'}}" - }'> - {{#and arrows (if carousel.slides.length '>' 1)}} - - {{/and}} +> {{#each carousel.slides}} {{#if button_text}} -
+
{{else}} - + {{/if}} diff --git a/templates/layout/base.html b/templates/layout/base.html index f2c4993969..3848c82a2a 100644 --- a/templates/layout/base.html +++ b/templates/layout/base.html @@ -56,6 +56,9 @@ {{~inject 'validationDictionaryJSON' (langJson 'validation_messages')}} {{~inject 'validationFallbackDictionaryJSON' (langJson 'validation_fallback_messages')}} {{~inject 'validationDefaultDictionaryJSON' (langJson 'default_messages')}} + {{~inject 'carouselArrowAndDotAriaLabel' (lang 'carousel.arrow_and_dot_aria_label')}} + {{~inject 'carouselActiveDotAriaLabel' (lang 'carousel.active_dot_aria_label')}} + {{~inject 'carouselContentAnnounceMessage' (lang 'carousel.content_announce_message')}} diff --git a/templates/pages/home.html b/templates/pages/home.html index 1ae7a9cd88..729d3e06ed 100644 --- a/templates/pages/home.html +++ b/templates/pages/home.html @@ -13,9 +13,9 @@ --- {{#partial "hero"}} {{{region name="home_below_menu"}}} - {{#if carousel}} + {{#and carousel carousel.slides.length}} {{> components/carousel arrows=theme_settings.homepage_show_carousel_arrows play_pause_button=theme_settings.homepage_show_carousel_play_pause_button}} - {{/if}} + {{/and}} {{{region name="home_below_carousel"}}} {{/partial}}