Skip to content

DS-1445 | CTA Group component #501

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

Merged
merged 33 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
f92f335
Update next and storyblok packages
yvonnetangsu Jun 11, 2025
1feb942
Beginning using Momentum CTA components
yvonnetangsu Jun 12, 2025
2584031
Keep working on CtaLinks
yvonnetangsu Jun 12, 2025
97b7abb
ts errors and WIP
yvonnetangsu Jun 12, 2025
7f8193e
Gradient links and other styles; remove Masthead component
yvonnetangsu Jun 13, 2025
0ed568f
Button align and styles
yvonnetangsu Jun 13, 2025
47b3b91
CTA icons
yvonnetangsu Jun 13, 2025
66b89b3
Use CtaLink for local footer links
yvonnetangsu Jun 13, 2025
019dbfa
Remove flexbox wrapper from CtaContent
yvonnetangsu Jun 13, 2025
d736f67
Add variant prop for styles not mapped to SbCtaLink; special icon styles
yvonnetangsu Jun 13, 2025
2a2334f
Remove react loading skeleton and update sa11y
yvonnetangsu Jun 13, 2025
2fe8182
test
yvonnetangsu Jun 14, 2025
83706f2
Update storyblok packages
yvonnetangsu Jun 14, 2025
fbdd656
Add editor check to access key in window URL for storyblokInit for St…
yvonnetangsu Jun 14, 2025
4eaaafb
Only do the editor check for the token in the URL for storyblok provider
yvonnetangsu Jun 14, 2025
32e4dde
test
yvonnetangsu Jun 14, 2025
e39893b
test
yvonnetangsu Jun 14, 2025
ae2658f
Undo
yvonnetangsu Jun 14, 2025
ac4b4db
try next 15.3.0
yvonnetangsu Jun 14, 2025
1fa4df8
Remove params of getStoryblokApi utility
yvonnetangsu Jun 14, 2025
d146b31
fixup URL param for editor token
yvonnetangsu Jun 14, 2025
a0a4134
undo accidentally changed path of not-found
yvonnetangsu Jun 14, 2025
62c8906
Merge branch 'next' into DS-1499_cta
yvonnetangsu Jun 15, 2025
391e44f
test
yvonnetangsu Jun 15, 2025
196a548
Add placeholder components
yvonnetangsu Jun 15, 2025
28595bc
import storyblok things from rsc directory
yvonnetangsu Jun 15, 2025
cb568ea
Update components/Storyblok/SbRowOneColumn.tsx
yvonnetangsu Jun 16, 2025
32430be
Update components/Storyblok/SbRowOneColumn.tsx
yvonnetangsu Jun 16, 2025
ad9475c
Extract reusable subcomponent for link groups
yvonnetangsu Jun 16, 2025
063200c
CtaGroup component
yvonnetangsu Jun 16, 2025
5cafd01
Merge branch 'next' into DS-1445_cta-group
yvonnetangsu Jun 16, 2025
ee4d4c8
use destructured linkText prop
yvonnetangsu Jun 16, 2025
b492e07
Reduce leading in CTAs so it more closely match the live site
yvonnetangsu Jun 16, 2025
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
7 changes: 5 additions & 2 deletions components/CreateBloks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { StoryblokServerComponent, type SbBlokData } from '@storyblok/react/rsc'

type CreateBloksProps = {
blokSection: SbBlokData[];
isListItems?: boolean;
[k: string]: unknown;
};

export const CreateBloks = ({ blokSection, ...props }: CreateBloksProps) => {
if (blokSection) {
export const CreateBloks = ({ blokSection, isListItems, ...props }: CreateBloksProps) => {
if (blokSection && isListItems) {
return blokSection.map((blok) => <li key={blok._uid}><StoryblokServerComponent blok={blok} {...props} /></li>);
} else if (blokSection) {
return blokSection.map((blok) => <StoryblokServerComponent key={blok._uid} blok={blok} {...props} />);
}

Expand Down
111 changes: 111 additions & 0 deletions components/Cta/Cta.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { type CtaIconLeftMarginType } from './Cta.types';

export const cta = 'group/cta transition-all';
export const buttonBase = 'block font-normal w-fit no-underline hocus:underline';
// hocus to plum dark gradient instead of solid plum dark to avoid a flash of white background on hocus
export const gradientButtonBase = 'bg-gradient-to-tr hocus:from-plum-dark hocus:to-plum-dark text-white hocus:text-white';
export const textLinkBase = 'block font-semibold w-fit no-underline text-18 md:text-20';
export const gradientTextLinkBase = 'bg-clip-text bg-gradient-to-tr text-transparent hocus:text-transparent';

// Maps to linkButtonStyle props in SbCtaLink. Only used for the Button style.
export const ctaButtonStyles = {
// Primary
'ood-cta__button--primary su-after-bg-white': 'bg-bay-dark text-white hocus:bg-palo-alto hocus:text-white',
// Secondary
'ood-cta__button--secondary su-after-bg-bay-dark su-after-bg-hocus-white': 'bg-white text-bay-dark shadow-bay-dark shadow-[inset_0_0_0_1px] after:text-bay-dark after:bg-bay-dark hocus:bg-bay-dark hocus:text-white hocus:after:text-white ',
// Give Now Button
'su-bg-digital-red su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white': 'bg-digital-red hocus:bg-plum-dark text-white hocus:text-white',
// Secondary Give Now Button
'su-bg-white su-bg-hocus-plum-dark su-text-digital-red su-text-hocus-white su-after-bg-digital-red su-after-bg-hocus-white': 'bg-white hocus:bg-plum-dark text-digital-red hocus:text-white after:bg-digital-red ',
// Ghost Button
'ood-cta__button--ghost su-after-bg-white': 'text-white bg-transparent shadow-white shadow-[inset_0_0_0_1px] transition-shadow hocus:text-white hocus:shadow-[inset_0_0_0_3px]',
// Solid Campaign Plum Button
'su-bg-plum su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white': 'bg-plum hocus:bg-plum-dark text-white hocus:text-white',
// Gradient Campaign Buttons
'su-bg-cardinal-dark-to-spirited-dark su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white su-transition-none': `${gradientButtonBase} from-cardinal-red-dark to-spirited-dark`,
'su-bg-plum-to-digital-red su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white su-transition-none': `${gradientButtonBase} from-plum to-digital-red`,
'su-bg-plum-to-spirited-dark su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white su-transition-none': `${gradientButtonBase} from-plum to-spirited-dark`,
'su-bg-palo-alto-dark-to-palo-verde-dark su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white su-transition-none': `${gradientButtonBase} from-palo-alto-dark to-palo-verde-dark`,
'su-bg-sky-dark-to-olive-dark su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white su-transition-none': `${gradientButtonBase} from-sky-dark to-olive-dark`,
'su-bg-sky-dark-to-bay-dark su-bg-hocus-plum-dark su-text-white su-text-hocus-white su-after-bg-white su-after-bg-hocus-white su-transition-none': `${gradientButtonBase} from-sky-dark to-bay-dark`,
};

// Maps to linkTextColor prop in SbCtaLink. Only used for the text link style.
export const ctaTextColors = {
'su-text-digital-red su-after-bg-digital-red su-text-hocus-sky-dark su-after-bg-hocus-sky-dark': 'text-digital-red hocus:text-sky-dark',
'su-text-white su-text-hocus-white su-hocus-underline su-after-bg-white su-after-bg-hocus-white': 'text-white hocus:text-white',
// Gradient text links for Campaign pages
'ood-cta__link-gradient su-bg-sky-dark-to-bay-dark su-after-bg-sky-dark-to-bay-dark': `${gradientTextLinkBase} from-sky-dark to-bay-dark *:[&_svg]:text-bay-dark *:[&_svg]:hocus:text-bay-dark`,
'ood-cta__link-gradient su-bg-cardinal-dark-to-spirited-dark su-after-bg-cardinal-dark-to-spirited-dark': `${gradientTextLinkBase} from-cardinal-red-dark to-spirited-dark *:[&_svg]:text-spirited-dark *:[&_svg]:hocus:text-spirited-dark`,
'ood-cta__link-gradient su-bg-plum-to-digital-red su-after-bg-plum-to-digital-red': `${gradientTextLinkBase} from-plum to-digital-red *:[&_svg]:text-digital-red *:[&_svg]:hocus:text-digital-red`,
'ood-cta__link-gradient su-bg-plum-to-spirited-dark su-after-bg-plum-to-spirited-dark': `${gradientTextLinkBase} from-plum to-spirited-dark *:[&_svg]:text-spirited-dark *:[&_svg]:hocus:text-spirited-dark`,
'ood-cta__link-gradient su-bg-palo-alto-dark-to-palo-verde-dark su-after-bg-palo-alto-dark-to-palo-verde-dark': `${gradientTextLinkBase} from-palo-alto-dark to-palo-verde-dark *:[&_svg]:text-palo-verde-dark *:[&_svg]:hocus:text-palo-verde-dark`,
'ood-cta__link-gradient su-bg-sky-dark-to-olive-dark su-after-bg-sky-dark-to-olive-dark': `${gradientTextLinkBase} from-sky-dark to-olive-dark *:[&_svg]:text-olive-dark *:[&_svg]:hocus:text-olive-dark`,
// Has an extra su-after-bg-sky-dark-to-bay-dark in SB, but it seems to work here without the dupe string
'ood-cta__link-gradient su-bg-sky-dark-to-bay-dark': `${gradientTextLinkBase} from-sky-dark to-bay-dark *:[&_svg]:text-bay-dark *:[&_svg]:hocus:text-bay-dark`,
/**
* Campaign page only solid text colors - seems on live site the intent was to use plum-dark as the hocus color, but it was overridden by the base link hocus color
* Here we honor the original intent by using plum-dark as the hocus color
*/
'su-text-lagunita-dark su-after-bg-lagunita-dark su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-lagunita-dark hocus:text-plum-dark',
'su-text-palo-verde su-after-bg-palo-verde su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-palo-verde hocus:text-plum-dark',
'su-text-plum su-after-bg-plum su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-plum hocus:text-plum-dark',
'su-text-brick su-after-bg-brick su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-brick hocus:text-plum-dark',
'su-text-cardinal-red su-after-bg-cardinal-red su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-cardinal-red hocus:text-plum-dark',
'su-text-palo-alto su-after-bg-palo-alto su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-palo-alto hocus:text-plum-dark',
'su-text-bay-dark su-after-bg-bay-dark su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-bay-dark hocus:text-plum-dark',
'su-text-sky-dark su-after-bg-sky-dark su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-sky-dark hocus:text-plum-dark',
'su-text-lagunita su-after-bg-lagunita su-text-hocus-plum-dark su-after-bg-hocus-plum-dark': 'text-lagunita hocus:text-plum-dark',
};

// Additional CTA variants we use for this site, e.g., as subcomponents for other components. These include styles for sizes, colors, icon styles, and other properties.
export const ctaVariants = {
'local-footer': 'text-digital-red hocus:text-black underline leading-snug font-normal text-16 md:text-18 *:[&_svg]:hocus:text-digital-red',
};

// Maps to linkButtonSize prop in SbCtaLink. Only used for the button styles
export const ctaButtonSizes = {
default: 'pt-11 pb-12 px-30 text-18 md:text-20',
'ood-cta__button--medium': 'pt-11 pb-12 px-30 md:py-14 md:px-34 text-20 md:text-24',
'ood-cta__button--large': 'py-16 px-30 md:py-20 md:px-36 text-22 md:text-28',
};

// Maps to linkIcon prop in SbCtaLink
export const ctaIcons = {
'su-link--action': 'chevron-right',
'su-link--jump': 'chevron-down',
'su-link--external': 'external',
'su-link--internal': 'lock',
'su-link--download': 'download',
'su-link--video': 'video',
'su-link--no-icon': '',
};

// Common styles for CTA icons
export const icon = 'inline-block will-change-transform transition-transform stroke-2';

// Icons have left margins
// Only add to this map if left margin is different from default class ml-04em
export const iconLeftMarginDefault = 'ml-04em';
export const iconLeftMargin: CtaIconLeftMarginType = {
'su-link--action': 'ml-03em',
};

// Maps to linkIcon prop in SbCtaLink. Animation preselected based on the icon type
export const iconAnimations = {
'su-link--action': 'group-hover/cta:translate-x-02em group-focus-visible/cta:translate-x-02em',
'su-link--jump': 'group-hover/cta:translate-y-02em group-focus-visible/cta:translate-y-02em',
'su-link--external': 'group-hover/cta:translate-x-01em group-focus-visible/cta:translate-x-01em group-hover/cta:-translate-y-01em group-focus-visible/cta:-translate-y-01em',
'su-link--internal': 'group-hover/cta:fill-current',
'su-link--download': 'group-hover/cta:translate-y-02em group-focus-visible/cta:translate-y-02em',
'su-link--video': 'group-hover/cta:translate-x-02em group-focus-visible/cta:translate-x-02em',
'su-link--no-icon': '',
};

export const ctaAligns = {
left: 'su-text-left',
center: 'su-text-center mx-auto',
right: 'su-text-right ml-auto mr-0',
};

export const ctaGroup = 'list-unstyled gap-x-08em gap-y-1em [&_li]:mb-0 [&_a]:text-09em [&_a]:md:text-20 [&_a]:p-07em [&_a]:md:pt-11 [&_a]:md:pb-12 [&_a]:md:px-30';
33 changes: 33 additions & 0 deletions components/Cta/Cta.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { type HeroIconProps } from '@/components/HeroIcon';
import { type MarginType } from '@/utilities/datasource';
import * as styles from './Cta.styles';

export type CtaButtonStyleType = keyof typeof styles.ctaButtonStyles;
export type CtaTextColorType = keyof typeof styles.ctaTextColors;
export type CtaButtonSizeType = keyof typeof styles.ctaButtonSizes;
export type CtaVariantType = keyof typeof styles.ctaVariants;
export type CtaAlignType = keyof typeof styles.ctaAligns;

export type CtaIconType = keyof typeof styles.ctaIcons;
export type IconAnimationType = keyof typeof styles.iconAnimations | '';

export type CtaIconLeftMarginType = Partial<{
[Key in CtaIconType]: string;
}>;

export interface CtaCommonProps {
srText?: string;
icon?: CtaIconType;
isButton?: boolean;
textColor?: CtaTextColorType;
buttonSize?: CtaButtonSizeType;
buttonStyle?: CtaButtonStyleType;
variant?: CtaVariantType;
align?: CtaAlignType;
iconProps?: Omit<HeroIconProps, 'icon'> & React.ComponentProps<'svg'>;
mt?: MarginType;
mb?: MarginType;
children?: React.ReactNode;
}

export type CtaGroupDisplayType = 'inline' | 'inline-block';
60 changes: 60 additions & 0 deletions components/Cta/CtaButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
'use client';
import React from 'react';
import { cnb } from 'cnbuilder';
import { CtaContent } from './CtaContent';
import { type CtaCommonProps } from './Cta.types';
import { marginTops, marginBottoms } from '@/utilities/datasource';
import * as styles from './Cta.styles';

export type CtaButtonProps = React.ComponentPropsWithoutRef<'button'> & CtaCommonProps;

export const CtaButton = React.forwardRef<HTMLButtonElement, CtaButtonProps>((props, ref) => {
const {
type = 'button',
isButton,
buttonStyle = 'ood-cta__button--primary su-after-bg-white',
buttonSize = 'default',
textColor = 'su-text-digital-red su-after-bg-digital-red su-text-hocus-sky-dark su-after-bg-hocus-sky-dark',
variant,
align,
icon,
iconProps,
srText,
mt,
mb,
children,
className,
...rest
} = props;

return (
<button
{...rest}
type={type}
ref={ref as React.ForwardedRef<HTMLButtonElement>}
className={cnb(
styles.cta,
styles.ctaAligns[align],
isButton ? styles.buttonBase : '',
!isButton && !variant ? styles.textLinkBase : '',
isButton ? styles.ctaButtonStyles[buttonStyle] : '',
isButton ? styles.ctaButtonSizes[buttonSize] : '',
!isButton && !variant ? styles.ctaTextColors[textColor] : '',
variant ? styles.ctaVariants[variant] : '',
mt ? marginTops[mt] : '',
mb ? marginBottoms[mb] : '',
className,
)}
>
<CtaContent
buttonStyle={buttonStyle}
icon={icon}
iconProps={iconProps}
srText={srText}
align={align}
>
{children}
</CtaContent>
</button>
);
});
37 changes: 37 additions & 0 deletions components/Cta/CtaContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { cnb } from 'cnbuilder';
import { HeroIcon, type IconType } from '@/components/HeroIcon';
import { SrOnlyText } from '@/components/Typography';
import * as styles from './Cta.styles';
import * as types from './Cta.types';

type CtaContentProps = Omit<types.CtaCommonProps, 'buttonSize' | 'textColor'>;

export const CtaContent = ({
icon = 'su-link--action',
iconProps,
srText,
children,
}: CtaContentProps) => {
const heroIcon = icon ? styles.ctaIcons[icon] as IconType : undefined;
const iconMarginLeft = children && icon ? styles.iconLeftMargin[icon] || styles.iconLeftMarginDefault : '';
const { className: iconClasses, ...iProps } = iconProps || {};
const iconAnimate = icon ? styles.iconAnimations[icon] : '';

return (
<>
{children}
{/* Use this whitespace-nowrap trick so icon won't get pushed to the next line on its own */}
{heroIcon && (
<span className="whitespace-nowrap">
&#65279;
<HeroIcon
icon={heroIcon}
className={cnb(styles.icon, iconAnimate, iconMarginLeft, iconClasses)}
{...iProps}
/>
</span>
)}
{srText && <SrOnlyText>{srText}</SrOnlyText>}
</>
);
};
78 changes: 78 additions & 0 deletions components/Cta/CtaExternalLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';
import React, { useEffect, useState } from 'react';
import { cnb } from 'cnbuilder';
import { CtaContent } from './CtaContent';
import { type CtaCommonProps } from './Cta.types';
import { type SbLinkType } from '../Storyblok/Storyblok.types';
import { marginTops, marginBottoms } from '@/utilities/datasource';
import * as styles from './Cta.styles';
import useUTMs from '@/hooks/useUTMs';

export type CtaExternalLinkProps = React.ComponentPropsWithoutRef<'a'> & CtaCommonProps & {
sbLink?: SbLinkType;
href?: string;
rel?: string;
};

export const CtaExternalLink = React.forwardRef<HTMLAnchorElement, CtaExternalLinkProps>((props, ref) => {
const {
isButton,
buttonStyle = 'ood-cta__button--primary su-after-bg-white',
buttonSize = 'default',
textColor = 'su-text-digital-red su-after-bg-digital-red su-text-hocus-sky-dark su-after-bg-hocus-sky-dark',
variant,
icon,
iconProps,
align,
srText,
rel,
mt,
mb,
children,
className,
href,
...rest
} = props;

// Add UTM params to Stanford URLs.
const { isStanfordUrl, addUTMsToUrl } = useUTMs();
const [myHref, setMyHref] = useState<string>(href);
useEffect(() => {
if (typeof window === 'undefined') return;
if (isStanfordUrl(href)) {
setMyHref(addUTMsToUrl(href));
}
}, [href, isStanfordUrl, addUTMsToUrl]);

return (
<a
{...rest}
href={myHref}
rel={rel}
ref={ref as React.ForwardedRef<HTMLAnchorElement>}
className={cnb(
styles.cta,
styles.ctaAligns[align],
isButton ? styles.buttonBase : '',
!isButton && !variant ? styles.textLinkBase : '',
isButton ? styles.ctaButtonStyles[buttonStyle] : '',
isButton ? styles.ctaButtonSizes[buttonSize] : '',
!isButton && !variant ? styles.ctaTextColors[textColor] : '',
variant ? styles.ctaVariants[variant] : '',
mt ? marginTops[mt] : '',
mb ? marginBottoms[mb] : '',
className,
)}
>
<CtaContent
buttonStyle={buttonStyle}
icon={icon}
iconProps={iconProps}
srText={srText}
align={align}
>
{children}
</CtaContent>
</a>
);
});
28 changes: 28 additions & 0 deletions components/Cta/CtaGroup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { cnb } from 'cnbuilder';
import { FlexBox } from '@/components/FlexBox';
import * as styles from './Cta.styles';
import * as types from './Cta.types';

type CtaGroupProps = React.HTMLAttributes<HTMLDivElement> & {
display: types.CtaGroupDisplayType;
};

export const CtaGroup = ({
display = 'inline-block',
className,
children,
...props
}: CtaGroupProps) => {
return (
<FlexBox
{...props}
as="ul"
direction={display === 'inline-block' ? 'row' : 'col'}
wrap={display === 'inline-block' ? 'wrap' : 'nowrap'}
justifyContent={display === 'inline-block' ? 'center' : 'start'}
className={cnb(styles.ctaGroup, className)}
>
{children}
</FlexBox>
);
};
Loading