Skip to content

Commit

Permalink
[SSP-2867] - disable link click on product list item text select text…
Browse files Browse the repository at this point in the history
… (#3593)
  • Loading branch information
TomasGottvald authored Dec 17, 2024
1 parent 3a52c56 commit 06978de
Show file tree
Hide file tree
Showing 6 changed files with 111 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export type ExtendedNextLinkProps = Omit<ComponentPropsWithoutRef<'a'>, keyof Li
queryParams?: Record<string, string>;
type?: PageType;
skeletonType?: PageType;
onClickExtended?: MouseEventHandler<HTMLAnchorElement>;
};

export const ExtendedNextLink: FC<ExtendedNextLinkProps> = ({
Expand All @@ -26,6 +27,7 @@ export const ExtendedNextLink: FC<ExtendedNextLinkProps> = ({
queryParams,
as,
onClick,
onClickExtended,
type,
skeletonType,
...props
Expand Down Expand Up @@ -65,7 +67,7 @@ export const ExtendedNextLink: FC<ExtendedNextLinkProps> = ({
}
: href
}
onClick={handleOnClick}
onClick={onClickExtended ?? handleOnClick}
{...props}
>
{children}
Expand Down
28 changes: 21 additions & 7 deletions storefront/components/Blocks/Banners/BannersSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Banner } from './Banner';
import { BannersDot } from './BannersDot';
import { bannersReducer } from './bannersUtils';
import { ExtendedNextLink } from 'components/Basic/ExtendedNextLink/ExtendedNextLink';
import { TIDs } from 'cypress/tids';
import { TypeSliderItemFragment } from 'graphql/requests/sliderItems/fragments/SliderItemFragment.generated';
import { useEffect, useReducer, useRef } from 'react';
import { useEffect, useReducer, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twJoin } from 'tailwind-merge';
import { isTextSelected, preventClick } from 'utils/ui/disableClickWhenTextSelected';

const SLIDER_STOP_SLIDE_TIMEOUT = 50 as const;
const SLIDER_STOP_SLIDE_TIMEOUT = 300 as const;
const SLIDER_SLIDE_DURATION = 500 as const;
const SLIDER_AUTOMATIC_SLIDE_INTERVAL = 5000 as const;

Expand All @@ -22,13 +24,16 @@ export const BannersSlider: FC<BannersSliderProps> = ({ sliderItems }) => {
isSliding: false,
slideDirection: 'NEXT',
});
const [isMoved, setIsMoved] = useState(false);
const intervalRef = useRef<NodeJS.Timeout | null>(null);

const slide = (dir: 'PREV' | 'NEXT') => {
setIsMoved(true);
checkAndClearInterval();
dispatchBannerSliderStateChange({ type: dir, numItems });
setTimeout(() => {
dispatchBannerSliderStateChange({ type: 'STOP_SLIDING' });
setIsMoved(false);
}, SLIDER_STOP_SLIDE_TIMEOUT);
startInterval();
};
Expand Down Expand Up @@ -59,8 +64,8 @@ export const BannersSlider: FC<BannersSliderProps> = ({ sliderItems }) => {
};

const handlers = useSwipeable({
onSwipedLeft: () => slide('NEXT'),
onSwipedRight: () => slide('PREV'),
onSwipedLeft: () => !isTextSelected() && slide('NEXT'),
onSwipedRight: () => !isTextSelected() && slide('PREV'),
preventScrollOnSwipe: true,
onTouchStartOrOnMouseDown: checkAndClearInterval,
trackMouse: true,
Expand All @@ -71,14 +76,23 @@ export const BannersSlider: FC<BannersSliderProps> = ({ sliderItems }) => {
"vl:[-ms-overflow-style:'none'] vl:[scrollbar-width:'none'] vl:[&::-webkit-scrollbar]:hidden",
);

const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (isTextSelected() || isMoved) {
preventClick(e);
}
};

return (
<div className="flex flex-col" tid={TIDs.banners_slider}>
<a
<ExtendedNextLink
{...handlers}
className="!no-underline"
className="select-text !no-underline"
draggable={false}
href={sliderItems[bannerSliderState.sliderPosition].link}
title={sliderItems[bannerSliderState.sliderPosition].name}
onClick={handleClick}
onMouseEnter={checkAndClearInterval}
onMouseUp={handleClick}
onMouseLeave={() => {
checkAndClearInterval();
startInterval();
Expand Down Expand Up @@ -107,7 +121,7 @@ export const BannersSlider: FC<BannersSliderProps> = ({ sliderItems }) => {
))}
</div>
</div>
</a>
</ExtendedNextLink>
<div
className={twJoin(
'relative',
Expand Down
112 changes: 57 additions & 55 deletions storefront/components/Blocks/Product/ProductsList/ProductListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { forwardRef } from 'react';
import { twJoin } from 'tailwind-merge';
import { FunctionComponentProps } from 'types/globals';
import { twMergeCustom } from 'utils/twMerge';
import { disableClickWhenTextSelected } from 'utils/ui/disableClickWhenTextSelected';

export type ProductVisibleItemsConfigType = {
addToCart?: boolean;
Expand Down Expand Up @@ -73,7 +74,7 @@ export const ProductListItem = forwardRef<HTMLLIElement, ProductItemProps>(
ref={ref}
tid={TIDs.blocks_product_list_listeditem_ + product.catalogNumber}
className={twMergeCustom(
'group relative flex select-none flex-col gap-2.5 rounded-xl border border-backgroundMore bg-backgroundMore px-2.5 py-5 text-left transition sm:px-5',
'group relative flex select-none flex-col gap-2.5 rounded-xl border border-backgroundMore bg-backgroundMore pb-2.5 text-left transition sm:pb-5',
size === 'small' && 'p-5',
'hover:border-borderAccentLess hover:bg-background',
className,
Expand All @@ -94,11 +95,12 @@ export const ProductListItem = forwardRef<HTMLLIElement, ProductItemProps>(
)}

<ExtendedNextLink
className="flex h-full select-none flex-col gap-2.5 text-text no-underline hover:text-link hover:no-underline"
className="flex h-full select-text flex-col py-5 text-text no-underline hover:text-link hover:no-underline sm:pb-0"
draggable={false}
href={product.slug}
type={product.isMainVariant ? 'productMainVariant' : 'product'}
onClick={() => {
onClickExtended={disableClickWhenTextSelected}
onMouseUp={() => {
onGtmProductClickEventHandler(
product,
gtmProductListName,
Expand All @@ -109,67 +111,67 @@ export const ProductListItem = forwardRef<HTMLLIElement, ProductItemProps>(
onClick?.(product, listIndex);
}}
>
<ProductListItemImage product={product} size={size} visibleItemsConfig={visibleItemsConfig} />
<div className="flex h-full flex-col gap-2.5 px-2.5 sm:px-5">
<ProductListItemImage product={product} size={size} visibleItemsConfig={visibleItemsConfig} />

<div
className={twJoin(
'grow overflow-hidden break-words font-secondary font-semibold group-hover:text-link group-hover:underline',
textSize === 'xs' ? 'text-xs' : 'text-sm',
)}
>
{product.fullName}
</div>

{product.__typename === 'MainVariant' && (
<div className="flex w-fit items-center gap-1.5 whitespace-nowrap rounded-md bg-background px-2.5 py-1.5 font-secondary text-xs group-hover:text-text">
<VariantIcon className="size-3 text-textAccent" />
{product.variantsCount} {t('variants count', { count: product.variantsCount })}
<div
className={twJoin(
'grow overflow-hidden break-words font-secondary font-semibold group-hover:text-link group-hover:underline',
textSize === 'xs' ? 'text-xs' : 'text-sm',
)}
>
{product.fullName}
</div>
)}

{visibleItemsConfig.price && !(product.isMainVariant && product.isSellingDenied) && (
<ProductPrice
className="min-h-6 sm:min-h-7"
isPriceFromVisible={visibleItemsConfig.priceFromWord}
productPrice={product.price}
/>
)}

{visibleItemsConfig.storeAvailability && (
<ProductAvailability
availability={product.availability}
availableStoresCount={product.availableStoresCount}
className="min-h-10 xs:min-h-[60px] sm:min-h-10"
isInquiryType={product.isInquiryType}
/>
)}
</ExtendedNextLink>
{product.__typename === 'MainVariant' && (
<div className="flex w-fit items-center gap-1.5 whitespace-nowrap rounded-md bg-background py-1.5 font-secondary text-xs group-hover:text-text">
<VariantIcon className="size-3 text-textAccent" />
{product.variantsCount} {t('variants count', { count: product.variantsCount })}
</div>
)}

{(visibleItemsConfig.addToCart || visibleItemsConfig.productListButtons) && (
<div className="flex w-full items-center justify-between gap-1 sm:justify-normal sm:gap-2.5">
{visibleItemsConfig.addToCart && (
<ProductAction
gtmMessageOrigin={gtmMessageOrigin}
gtmProductListName={gtmProductListName}
listIndex={listIndex}
product={product}
{visibleItemsConfig.price && !(product.isMainVariant && product.isSellingDenied) && (
<ProductPrice
className="min-h-6 sm:min-h-7"
isPriceFromVisible={visibleItemsConfig.priceFromWord}
productPrice={product.price}
/>
)}

{visibleItemsConfig.productListButtons && (
<>
<ProductCompareButton
isProductInComparison={isProductInComparison}
toggleProductInComparison={toggleProductInComparison}
/>
<ProductWishlistButton
isProductInWishlist={isProductInWishlist}
toggleProductInWishlist={toggleProductInWishlist}
/>
</>
{visibleItemsConfig.storeAvailability && (
<ProductAvailability
availability={product.availability}
availableStoresCount={product.availableStoresCount}
className="min-h-10 xs:min-h-[60px] sm:min-h-10"
isInquiryType={product.isInquiryType}
/>
)}
</div>
)}
</ExtendedNextLink>

<div className="flex w-full items-center justify-between gap-1 px-2.5 py-5 sm:justify-normal sm:gap-2.5 sm:px-5 sm:py-0">
{visibleItemsConfig.addToCart && (
<ProductAction
gtmMessageOrigin={gtmMessageOrigin}
gtmProductListName={gtmProductListName}
listIndex={listIndex}
product={product}
/>
)}

{visibleItemsConfig.productListButtons && (
<>
<ProductCompareButton
isProductInComparison={isProductInComparison}
toggleProductInComparison={toggleProductInComparison}
/>
<ProductWishlistButton
isProductInWishlist={isProductInWishlist}
toggleProductInWishlist={toggleProductInWishlist}
/>
</>
)}
</div>
</li>
);
},
Expand Down
11 changes: 8 additions & 3 deletions storefront/components/Blocks/Product/ProductsSlider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { RefObject, createRef, useEffect, useRef, useState } from 'react';
import { useSwipeable } from 'react-swipeable';
import { twJoin } from 'tailwind-merge';
import { twMergeCustom } from 'utils/twMerge';
import { isTextSelected } from 'utils/ui/disableClickWhenTextSelected';
import { isWholeElementVisible } from 'utils/ui/isWholeElementVisible';
import { useMediaMin } from 'utils/ui/useMediaMin';
import { wait } from 'utils/wait';
Expand Down Expand Up @@ -85,7 +86,9 @@ export const ProductsSlider: FC<ProductsSliderProps> = ({

const newActiveIndex = isFirstSlide ? productElementRefs!.length - 4 : prevIndex;

setActiveIndex(newActiveIndex);
if (!isTextSelected()) {
setActiveIndex(newActiveIndex);
}
};

const handleNext = () => {
Expand All @@ -98,7 +101,9 @@ export const ProductsSlider: FC<ProductsSliderProps> = ({

const newActiveIndex = isEndSlide ? 0 : nextIndex;

setActiveIndex(newActiveIndex);
if (!isTextSelected()) {
setActiveIndex(newActiveIndex);
}
};

const handlers = useSwipeable({
Expand Down Expand Up @@ -150,7 +155,7 @@ export const ProductsSlider: FC<ProductsSliderProps> = ({
])}
productItemProps={{
className: twMergeCustom(
'snap-center md:snap-start mx-1 md:mx-2 first:ml-0 last:mr-0',
'snap-center md:snap-start mx-1 md:mx-2 first:ml-0 last:mr-0 p-0',
productItemProps?.className,
),
...productItemProps,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { TIDs } from 'cypress/tids';
import { TypeListedProductFragment } from 'graphql/requests/products/fragments/ListedProductFragment.generated';
import { GtmProductListNameType } from 'gtm/enums/GtmProductListNameType';
import { onGtmProductClickEventHandler } from 'gtm/handlers/onGtmProductClickEventHandler';
import { twJoin } from 'tailwind-merge';
import { useFormatPrice } from 'utils/formatting/useFormatPrice';
import { isPriceVisible } from 'utils/mappers/price';
import { disableClickWhenTextSelected } from 'utils/ui/disableClickWhenTextSelected';

type CategoryBestsellersListItemProps = {
product: TypeListedProductFragment;
Expand All @@ -31,11 +31,11 @@ export const CategoryBestsellersListItem: FC<CategoryBestsellersListItemProps> =

return (
<ExtendedNextLink
className="flex items-center justify-between gap-5 gap-y-4 p-3 no-underline transition-colors hover:bg-background hover:no-underline"
draggable={false}
href={productUrl}
type={product.__typename === 'RegularProduct' ? 'product' : 'productMainVariant'}
className={twJoin(
'group flex items-center justify-between gap-5 gap-y-4 p-3 no-underline transition-colors hover:bg-background hover:no-underline',
)}
onClickExtended={disableClickWhenTextSelected}
onClick={() =>
onGtmProductClickEventHandler(
product,
Expand All @@ -54,7 +54,7 @@ export const CategoryBestsellersListItem: FC<CategoryBestsellersListItemProps> =
visibleItemsConfig={{ flags: false }}
/>
</div>
<div className="flex w-full flex-col justify-between gap-x-4 gap-y-2.5 md:flex-row md:items-center">
<div className="flex w-full select-text flex-col justify-between gap-x-4 gap-y-2.5 md:flex-row md:items-center">
<span className="line-clamp-5 max-w-80 flex-1 items-center font-secondary text-sm font-semibold text-text">
{!!product.flags.length && <ProductFlags flags={product.flags} variant="bestsellers" />}
{product.fullName}
Expand Down
17 changes: 17 additions & 0 deletions storefront/utils/ui/disableClickWhenTextSelected.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
export const isTextSelected = () => {
const selection = window.getSelection();
return selection && selection.toString().length > 0;
};

export const disableClickWhenTextSelected = (e: React.MouseEvent<HTMLAnchorElement>) => {
if (isTextSelected()) {
preventClick(e);
}
return true;
};

export const preventClick = (event: React.MouseEvent<HTMLAnchorElement>) => {
event.preventDefault();
event.stopPropagation();
return false;
};

0 comments on commit 06978de

Please sign in to comment.