Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to image carousel to allow editors to add optional CTA to each image #2610 #2714

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions sanityv3/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import fullWidthVideo from './objects/fullWidthVideo'
import iframe from './objects/iframe'
import imageWithAlt from './objects/imageWithAlt'
import imageWithAltAndCaption from './objects/imageWithAltAndCaption'
import carouselImage from './objects/carouselImage'
import largeTable from './objects/largeTable'
import linkSelector from './objects/linkSelector'
import menuGroup from './objects/menuGroup'
Expand Down Expand Up @@ -139,6 +140,7 @@ const RemainingSchemas = [
internalServerError,
imageWithAlt,
imageWithAltAndCaption,
carouselImage,
pullQuote,
factbox,
relatedLinks,
Expand Down
66 changes: 66 additions & 0 deletions sanityv3/schemas/objects/carouselImage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { ImageWithAlt } from './imageWithAlt'
import type { Reference } from 'sanity'
import { Rule } from 'sanity'


export type CarouselImage = {
_type: 'carouselImage'
image: ImageWithAlt
captionPositionUnderImage?: boolean
action?: Reference[]
}

export default {
name: 'carouselImage',
title: 'Image with options',
type: 'object',
options: {
collapsed: false,
},
fields: [
{
name: 'image',
title: 'Image with alt',
type: 'imageWithAlt',
},
{
name: 'caption',
title: 'Image caption',
type: 'string',
},
{
name: 'attribution',
title: 'Credit',
type: 'string',
},
{
type: 'boolean',
name: 'captionPositionUnderImage',
title: 'Position caption and credit under image',
description: 'Toggle to display caption and credit under the image.',
initialValue: false,
},
{
name: 'action',
title: 'Link',
type: 'array',
of: [{ type: 'linkSelector', title: 'Link' }],
description: 'Optional link associated with the image.',
validation: (Rule: Rule) => Rule.max(1).error('Only one action is permitted'),
},
],
preview: {
select: {
imageUrl: 'image.asset.url',
alt: 'image.alt',
caption: 'caption',
},
prepare({ imageUrl, caption, alt }: { imageUrl: string; alt: string; caption: string }) {
return {
title: alt || 'No alt text',
subtitle: caption || 'No caption',
media: <img src={imageUrl} alt={alt} style={{ height: '100%' }} />,
}
},
},
}
4 changes: 3 additions & 1 deletion sanityv3/schemas/objects/imageCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,9 @@ export default {
name: 'items',
description: 'Add images for the carousel',
title: 'Carousel items',
of: [{ type: 'imageWithAltAndCaption' }],
of: [{ type: 'imageWithAltAndCaption' },
{ type: 'carouselImage' }
],
validation: (Rule: Rule) => Rule.required().min(2),
},
{
Expand Down
5 changes: 3 additions & 2 deletions web/core/Carousel/Carousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export const Carousel = forwardRef<HTMLElement, CarouselProps>(function Carousel
displayMode={displayMode}
aria-label={ariaLabel}
active={i === currentIndex}
action={(item as ImageCarouselItem).action}
{...(variant === 'image' &&
displayMode === 'single' && {
style: {
Expand Down Expand Up @@ -357,9 +358,9 @@ export const Carousel = forwardRef<HTMLElement, CarouselProps>(function Carousel
variant === 'image' && displayMode === 'single'
? 'w-[var(--image-carousel-card-w-sm)] md:w-[var(--image-carousel-card-w-md)] lg:w-[var(--image-carousel-card-w-lg)] mx-auto col-start-1 col-end-1 row-start-2 row-end-2'
: ''
} pt-6 pb-2 ${items.length === 3 ? 'lg:hidden' : ''} flex ${
} pb-2 ${items.length === 3 ? 'lg:hidden' : ''} flex ${
internalAutoRotation ? 'justify-between' : 'justify-end'
}`}
} absolute bottom-10 left-0 right-0 z-10 min-w-0 mx-layout-sm`}
>
<div id={controlsId} className="sr-only">
<FormattedMessage id="carousel_controls" defaultMessage="Carousel controls" />
Expand Down
65 changes: 31 additions & 34 deletions web/core/Carousel/CarouselImageItem.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import envisTwMerge from '../../twMerge'
import Image from '../../pageComponents/shared/SanityImage'
import { ImageWithAlt, ImageWithCaptionData } from '../../types/index'
import { ImageWithAlt, ImageWithCaptionData, LinkData } from '../../types/index'
import { DisplayModes } from './Carousel'
import { forwardRef, HTMLAttributes } from 'react'
import GridLinkArrow from '@sections/Grid/GridLinkArrow'

type CarouselImageItemProps = {
image?: ImageWithAlt | ImageWithCaptionData
Expand All @@ -12,10 +13,21 @@ type CarouselImageItemProps = {
caption?: string
attribution?: string
active?: boolean
captionPositionUnderImage?: boolean
action?: LinkData
} & HTMLAttributes<HTMLLIElement>

export const CarouselImageItem = forwardRef<HTMLLIElement, CarouselImageItemProps>(function CarouselImageItem(
{ active = false, image, caption, attribution, displayMode = 'single', className = '', ...rest },
{
active = false,
image,
caption,
attribution,
displayMode = 'single',
className = '',
action,
captionPositionUnderImage,
...rest
},
ref,
) {
return (
Expand All @@ -27,49 +39,34 @@ export const CarouselImageItem = forwardRef<HTMLLIElement, CarouselImageItemProp
aria-roledescription="slide"
className={envisTwMerge(
`
aspect-4/5
md:aspect-video
relative
h-full
${
displayMode === 'single'
? `
transition-opacity
duration-1000
ease-[ease]`
: 'shrink-0'
}
${displayMode === 'single' ? 'transition-opacity duration-1000 ease-[ease]' : 'shrink-0'}
${!active && displayMode === 'single' ? 'opacity-30' : ''}
${
displayMode === 'scroll'
? 'w-[80%] snap-center scroll-ml-6'
: 'w-[var(--image-carousel-card-w-sm)] md:w-[var(--image-carousel-card-w-md)] lg:w-[var(--image-carousel-card-w-lg)] ms-2 me-2 col-start-1 col-end-1 row-start-1 row-end-1'
: 'w-[var(--image-carousel-card-w-sm)] md:w-[var(--image-carousel-card-w-md)] lg:w-[var(--image-carousel-card-w-lg)] ms-2 me-2'
}
`,
className,
)}
>
{caption || attribution ? (
<figure className="relative w-full h-full">
<Image maxWidth={1420} image={image as ImageWithAlt} fill className="rounded-md" />
<figcaption
className={`${
active ? 'block' : 'hidden'
} absolute bottom-0 left-4 right-4 lg:left-8 lg:right-8 mb-4 lg:mb-8`}
>
<div
className={`bg-spruce-wood-70/75 text-slate-80 px-8 pt-6 w-fit flex flex-col max-w-text ${
attribution ? 'pb-4' : 'pb-6'
}`}
>
{caption && <span className={`text-lg ${attribution ? 'pb-3' : ''}`}>{caption}</span>}
{attribution && <span className="text-sm">{attribution}</span>}
</div>
{/* Image Section */}
<div className="relative w-full aspect-4/5 md:aspect-video">
<Image maxWidth={1420} image={image as ImageWithAlt} fill className="rounded-md" />
<GridLinkArrow action={action} variant="circle" />
</div>

{/* Caption Section */}
{(caption || attribution) && (
<figure className="relative w-full mt-4 ml-10">
<figcaption className="bg-spruce-wood-70/75 text-slate-80 px-4 py-2 rounded-md flex flex-col">
{caption && <span className="text-lg">{caption}</span>}
{attribution && <span className="text-sm mt-2">{attribution}</span>}
</figcaption>
</figure>
) : (
<Image maxWidth={1420} image={image as ImageWithAlt} fill className="rounded-md" />
)}
</li>
)
})
);
});
11 changes: 8 additions & 3 deletions web/lib/queries/common/imageCarouselFields.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import linkSelectorFields from './actions/linkSelectorFields'
import background from './background'

export const imageCarouselFields = /* groq */ `
Expand All @@ -6,15 +7,19 @@ export const imageCarouselFields = /* groq */ `
title,
ingress,
hideTitle,
items[] {
items[]{
...,
"id": _key,
...
action[0]{
${linkSelectorFields},
}
},
"options": {
autoplay,
delay
},
"designOptions": {
captionPositionUnderImage,
"designOptions": {
${background}
},
`
70 changes: 41 additions & 29 deletions web/sections/Grid/GridLinkArrow.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,49 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { twMerge } from 'tailwind-merge'
import { getUrlFromAction } from '../../common/helpers'
import { BaseLink } from '@core/Link'
import { getLocaleFromName } from '../../lib/localization'
import { ArrowRight } from '../../icons'
import { LinkData } from '../../types/index'
import { forwardRef } from 'react'
import { twMerge } from 'tailwind-merge';
import { getUrlFromAction } from '../../common/helpers';
import { BaseLink } from '@core/Link';
import { getLocaleFromName } from '../../lib/localization';
import { ArrowRight } from '../../icons';
import { LinkData } from '../../types/index';
import { forwardRef } from 'react';

type GridLinkArrowProps = {
action?: LinkData
className?: string
bgColor?: string
}
action?: LinkData;
className?: string;
bgColor?: string;
variant?: "square" | "circle" ;
};

const GridLinkArrow = forwardRef<HTMLDivElement, GridLinkArrowProps>(function GridLinkArrow(
{ action, className = '', bgColor },
{ action, className = '', bgColor, variant = 'square' },
ref,
) {
const url = action && getUrlFromAction(action)
const url = action && getUrlFromAction(action);

const variantClassName = () => {
const bgClassName = () => {
switch (bgColor) {
case 'bg-yellow-50':
case 'bg-green-50':
case 'bg-orange-50':
case 'bg-mist-blue-100':
case 'bg-moss-green-50':
case 'bg-spruce-wood-90':
return `text-slate-80 hover:bg-slate-80 hover:text-white-100 focus-visible:bg-slate-80 focus-visible:text-white-100`
return `text-slate-80 hover:bg-slate-80 hover:text-white-100 focus-visible:bg-slate-80 focus-visible:text-white-100`;
case 'bg-white-100':
return `text-slate-80 hover:bg-grey-50 hover:text-white-100 focus-visible:bg-grey-50 focus-visible:text-white-100`
return `text-slate-80 hover:bg-grey-50 hover:text-white-100 focus-visible:bg-grey-50 focus-visible:text-white-100`;
case 'bg-blue-50':
case 'bg-slate-80':
default:
return `text-white-100 hover:bg-white-100 hover:text-slate-80 focus-visible:bg-white-100 focus-visible:text-slate-80`
return `text-white-100 hover:bg-white-100 hover:text-slate-80 focus-visible:bg-white-100 focus-visible:text-slate-80`;
}
};

const variantClassName = () => {
switch (variant) {
case 'circle':
return `m-1 p-2 hover:rounded-full`;
default:
return ``;
}
}

Expand All @@ -51,24 +61,26 @@ const GridLinkArrow = forwardRef<HTMLDivElement, GridLinkArrowProps>(function Gr
href={url as string}
{...(action.link?.lang && { locale: getLocaleFromName(action.link?.lang) })}
type={action.type}
className={`group
py-2
px-4
focus:outline-none
${variantClassName()}
focus-visible:envis-outline
dark:focus-visible:envis-outline
`}
className={twMerge(
`group
py-2
px-4
focus:outline-none
${bgClassName()}
${variantClassName()}
focus-visible:envis-outline
dark:focus-visible:envis-outline`,
)}
>
<span className="sr-only">{`${action.label} ${
action.extension ? `(${action.extension.toUpperCase()})` : ''
}`}</span>
<ArrowRight className={`size-10`} />
<ArrowRight className={variant === 'circle' ? 'size-7' :`size-10`} />
</BaseLink>
</div>
)}
</>
)
})
);
});

export default GridLinkArrow
export default GridLinkArrow;
2 changes: 2 additions & 0 deletions web/types/imageTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,6 @@ export type ImageCarouselData = {

export type ImageCarouselItem = {
id: string
captionPositionUnderImage?: boolean
action?: any
} & ImageWithCaptionData
Loading