diff --git a/src/__stories__/data-card-story.tsx b/src/__stories__/data-card-story.tsx index b3e3b71cce..48e06f78c5 100644 --- a/src/__stories__/data-card-story.tsx +++ b/src/__stories__/data-card-story.tsx @@ -32,8 +32,9 @@ type DataCardArgs = { title: string; subtitle: string; description: string; + ariaLabel: string; withExtra: boolean; - actions: 'button' | 'link' | 'button and link' | 'on press'; + actions: 'button' | 'link' | 'button and link' | 'onPress' | 'href' | 'to' | 'none'; closable: boolean; withTopAction: boolean; aspectRatio: AspectRatio; @@ -49,6 +50,7 @@ export const Default: StoryComponent = ({ title, subtitle, description, + ariaLabel, withExtra, actions = 'button', closable, @@ -66,22 +68,22 @@ export const Default: StoryComponent = ({ icon = ; } - const button = actions.includes('button') ? ( - - Action - - ) : undefined; - - const buttonLink = actions.includes('link') ? Link : undefined; - - const onPress = actions.includes('press') ? () => {} : undefined; - - const interactiveActions = onPress - ? {onPress} - : { - button, - buttonLink, - }; + const interactiveActions = { + button: actions.includes('button') ? ( + + Action + + ) : undefined, + buttonLink: actions.includes('link') ? Link : undefined, + onPress: actions === 'onPress' ? () => {} : undefined, + to: actions === 'to' ? '#' : undefined, + href: actions === 'href' ? 'https://example.org' : undefined, + } as + | {button?: JSX.Element; buttonLink?: JSX.Element} + | {onPress: () => void} + | {to: string} + | {href: string} + | {[key: string]: never}; const aspectRatioValue = fixedAspectRatioValues.includes(aspectRatio) ? aspectRatio.replace(' ', ':') @@ -100,7 +102,7 @@ export const Default: StoryComponent = ({ {...interactiveActions} aspectRatio={aspectRatioValue as AspectRatio} dataAttributes={{testid: 'data-card'}} - aria-label="Data card label" + aria-label={ariaLabel} actions={ withTopAction ? [ @@ -141,6 +143,7 @@ Default.args = { description: 'This is a description for the card', withExtra: false, actions: 'button', + ariaLabel: '', closable: false, withTopAction: false, aspectRatio: 'auto', @@ -155,7 +158,7 @@ Default.argTypes = { control: {type: 'select'}, }, actions: { - options: ['button', 'link', 'button and link', 'on press', 'none'], + options: ['button', 'link', 'button and link', 'onPress', 'href', 'to', 'none'], control: {type: 'select'}, }, aspectRatio: { diff --git a/src/__stories__/display-data-card-story.tsx b/src/__stories__/display-data-card-story.tsx index a013542186..a56a424083 100644 --- a/src/__stories__/display-data-card-story.tsx +++ b/src/__stories__/display-data-card-story.tsx @@ -37,7 +37,15 @@ type DisplayDataCardArgs = { withExtra: boolean; closable: boolean; withTopAction: boolean; - actions: 'button' | 'link' | 'button and link' | 'button and secondary button'; + actions: + | 'button' + | 'link' + | 'button and link' + | 'button and secondary button' + | 'onPress' + | 'href' + | 'to' + | 'none'; isInverse: boolean; aspectRatio: AspectRatio; }; @@ -73,28 +81,27 @@ export const Default: StoryComponent = ({ icon = ; } - const button = actions.includes('button') ? ( - - Action - - ) : undefined; - - const buttonLink = actions.includes('link') ? Link : undefined; - const secondaryButton = actions.includes('secondary') ? ( - - Action 2 - - ) : undefined; - - const onPress = actions.includes('press') ? () => {} : undefined; - - const interactiveActions = onPress - ? {onPress} - : { - button, - buttonLink, - secondaryButton, - }; + const interactiveActions = { + button: actions.includes('button') ? ( + + Action + + ) : undefined, + secondaryButton: actions.includes('secondary') ? ( + + Action 2 + + ) : undefined, + buttonLink: actions.includes('link') ? Link : undefined, + onPress: actions === 'onPress' ? () => {} : undefined, + to: actions === 'to' ? '#' : undefined, + href: actions === 'href' ? 'https://example.org' : undefined, + } as + | {button?: JSX.Element; buttonLink?: JSX.Element; secondaryButton?: JSX.Element} + | {onPress: () => void} + | {to: string} + | {href: string} + | {[key: string]: never}; const aspectRatioValue = fixedAspectRatioValues.includes(aspectRatio) ? aspectRatio.replace(' ', ':') @@ -166,7 +173,16 @@ Default.argTypes = { control: {type: 'select'}, }, actions: { - options: ['button', 'link', 'button and link', 'button and secondary button', 'on press'], + options: [ + 'button', + 'link', + 'button and link', + 'button and secondary button', + 'onPress', + 'href', + 'to', + 'none', + ], control: {type: 'select'}, }, aspectRatio: { diff --git a/src/__stories__/display-media-card-story.tsx b/src/__stories__/display-media-card-story.tsx index b7e820f769..f480f26ae7 100644 --- a/src/__stories__/display-media-card-story.tsx +++ b/src/__stories__/display-media-card-story.tsx @@ -44,7 +44,15 @@ type DisplayMediaCardArgs = { withExtra: boolean; closable: boolean; withTopAction: boolean; - actions: 'button' | 'link' | 'button and link' | 'button and secondary button' | 'onPress'; + actions: + | 'button' + | 'link' + | 'button and link' + | 'button and secondary button' + | 'onPress' + | 'href' + | 'to' + | 'none'; width: string; aspectRatio: '1:1' | '16:9' | '7:10' | '9:10' | 'auto'; isEmptySource: boolean; @@ -79,28 +87,27 @@ export const Default: StoryComponent = ({ icon = ; } - const button = actions.includes('button') ? ( - - Action - - ) : undefined; - - const buttonLink = actions.includes('link') ? Link : undefined; - const secondaryButton = actions.includes('secondary') ? ( - - Action 2 - - ) : undefined; - - const onPress = actions.includes('press') ? () => {} : undefined; - - const interactiveActions = onPress - ? {onPress} - : { - button, - buttonLink, - secondaryButton, - }; + const interactiveActions = { + button: actions.includes('button') ? ( + + Action + + ) : undefined, + secondaryButton: actions.includes('secondary') ? ( + + Action 2 + + ) : undefined, + buttonLink: actions.includes('link') ? Link : undefined, + onPress: actions === 'onPress' ? () => {} : undefined, + to: actions === 'to' ? '#' : undefined, + href: actions === 'href' ? 'https://example.org' : undefined, + } as + | {button?: JSX.Element; buttonLink?: JSX.Element; secondaryButton?: JSX.Element} + | {onPress: () => void} + | {to: string} + | {href: string} + | {[key: string]: never}; const backgroundProps = background === 'image' @@ -184,7 +191,16 @@ Default.argTypes = { control: {type: 'select'}, }, actions: { - options: ['button', 'link', 'button and link', 'button and secondary button', 'on press'], + options: [ + 'button', + 'link', + 'button and link', + 'button and secondary button', + 'onPress', + 'href', + 'to', + 'none', + ], control: {type: 'select'}, }, background: { diff --git a/src/__stories__/media-card-story.tsx b/src/__stories__/media-card-story.tsx index d74da74f59..c4f037a555 100644 --- a/src/__stories__/media-card-story.tsx +++ b/src/__stories__/media-card-story.tsx @@ -40,7 +40,7 @@ type Args = { subtitle: string; description: string; withExtra: boolean; - actions: 'button' | 'link' | 'button and link' | 'on press' | 'none'; + actions: 'button' | 'link' | 'button and link' | 'onPress' | 'href' | 'to' | 'none'; closable: boolean; withTopAction: boolean; isEmptySource: boolean; @@ -72,24 +72,23 @@ export const Default: StoryComponent = ({ icon = ; } - const button = actions.includes('button') ? ( - - Action - - ) : undefined; + const interactiveActions = { + button: actions.includes('button') ? ( + + Action + + ) : undefined, - const buttonLink = actions.includes('link') ? ( - Link - ) : undefined; - - const onPress = actions.includes('press') ? () => {} : undefined; - - const interactiveActions = onPress - ? {onPress} - : { - button, - buttonLink, - }; + buttonLink: actions.includes('link') ? Link : undefined, + onPress: actions === 'onPress' ? () => {} : undefined, + to: actions === 'to' ? '#' : undefined, + href: actions === 'href' ? 'https://example.org' : undefined, + } as + | {button?: JSX.Element; buttonLink?: JSX.Element; secondaryButton?: JSX.Element} + | {onPress: () => void} + | {to: string} + | {href: string} + | {[key: string]: never}; return ( = ({ icon = ; } - const button = actions.includes('button') ? ( - - Action - - ) : undefined; + const interactiveActions = { + button: actions.includes('button') ? ( + + Action + + ) : undefined, - const buttonLink = actions.includes('link') ? ( - Link - ) : undefined; - - const onPress = actions.includes('press') ? () => {} : undefined; - - const interactiveActions = onPress - ? {onPress} - : { - button, - buttonLink, - }; + buttonLink: actions.includes('link') ? Link : undefined, + onPress: actions === 'onPress' ? () => {} : undefined, + to: actions === 'to' ? '#' : undefined, + href: actions === 'href' ? 'https://example.org' : undefined, + } as + | {button?: JSX.Element; buttonLink?: JSX.Element; secondaryButton?: JSX.Element} + | {onPress: () => void} + | {to: string} + | {href: string} + | {[key: string]: never}; return ( @@ -180,7 +179,7 @@ Default.argTypes = { control: {type: 'select'}, }, actions: { - options: ['button', 'link', 'button and link', 'on press', 'none'], + options: ['button', 'link', 'button and link', 'onPress', 'href', 'to', 'none'], control: {type: 'select'}, }, }; diff --git a/src/__stories__/poster-card-story.tsx b/src/__stories__/poster-card-story.tsx index 1893291964..8e857472cc 100644 --- a/src/__stories__/poster-card-story.tsx +++ b/src/__stories__/poster-card-story.tsx @@ -129,7 +129,7 @@ export const Default: StoryComponent = ({ variant, }; - const touchableProps = { + const interactiveProps = { onPress: actions === 'onPress' ? () => {} : undefined, to: actions === 'to' ? '#' : undefined, href: actions === 'href' ? 'https://example.org' : undefined, @@ -152,7 +152,7 @@ export const Default: StoryComponent = ({ width={width} height={height} aspectRatio={aspectRatio} - {...touchableProps} + {...interactiveProps} /> diff --git a/src/__stories__/snap-card-story.tsx b/src/__stories__/snap-card-story.tsx index 973f4795b6..3c39d4d700 100644 --- a/src/__stories__/snap-card-story.tsx +++ b/src/__stories__/snap-card-story.tsx @@ -25,7 +25,7 @@ type Args = { title: string; subtitle: string; description: string; - actions: 'on press' | 'none'; + actions: 'onPress' | 'href' | 'to' | 'none'; isInverse: boolean; withExtra: boolean; aspectRatio: AspectRatio; @@ -65,6 +65,12 @@ export const Default: StoryComponent = ({ ? aspectRatio.replace(' ', ':') : aspectRatio; + const interactiveProps = { + onPress: actions === 'onPress' ? () => {} : undefined, + to: actions === 'to' ? '#' : undefined, + href: actions === 'href' ? 'https://example.org' : undefined, + } as {onPress: () => void} | {to: string} | {href: string} | {[key: string]: never}; + return ( = ({ aria-label="SnapCard card label" isInverse={isInverse} extra={withExtra ? : undefined} - onPress={ - actions === 'on press' - ? () => { - window.alert('SnapCard clicked'); - } - : undefined - } aspectRatio={aspectRatioValue as AspectRatio} + {...interactiveProps} /> ); }; @@ -116,7 +116,7 @@ Default.argTypes = { }, }, actions: { - options: ['on press', 'none'], + options: ['onPress', 'href', 'to', 'none'], control: {type: 'select'}, }, }; diff --git a/src/__tests__/data-card-test.tsx b/src/__tests__/data-card-test.tsx new file mode 100644 index 0000000000..abbe4b39d5 --- /dev/null +++ b/src/__tests__/data-card-test.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import {DataCard} from '../card'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; +import Tag from '../tag'; +import Stack from '../stack'; +import {Text2} from '../text'; + +test('DataCard "href" label', async () => { + render( + + Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); + +test('DataCard "to" label', async () => { + render( + + Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); + +test('DataCard "onPress" label', async () => { + render( + + {}} + headline={Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); diff --git a/src/__tests__/display-data-card-test.tsx b/src/__tests__/display-data-card-test.tsx new file mode 100644 index 0000000000..02e5e647e2 --- /dev/null +++ b/src/__tests__/display-data-card-test.tsx @@ -0,0 +1,74 @@ +import * as React from 'react'; +import {DisplayDataCard} from '../card'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; +import Tag from '../tag'; +import Stack from '../stack'; +import {Text2} from '../text'; + +test('DisplayDataCard "href" label', async () => { + render( + + Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); + +test('DisplayDataCard "to" label', async () => { + render( + + Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); + +test('DisplayDataCard "onPress" label', async () => { + render( + + {}} + headline={Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); diff --git a/src/__tests__/display-media-card-test.tsx b/src/__tests__/display-media-card-test.tsx new file mode 100644 index 0000000000..21297fdf22 --- /dev/null +++ b/src/__tests__/display-media-card-test.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import {DisplayMediaCard} from '../card'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; +import Tag from '../tag'; +import Stack from '../stack'; +import {Text2} from '../text'; + +test('DisplayMediaCard "href" label', async () => { + render( + + Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); + +test('DisplayMediaCard "to" label', async () => { + render( + + Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); + +test('DisplayMediaCard "onPress" label', async () => { + render( + + {}} + backgroundImage="https://source.unsplash.com/900x900/" + headline={Headline} + pretitle="Pretitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); +}); diff --git a/src/__tests__/media-card-test.tsx b/src/__tests__/media-card-test.tsx new file mode 100644 index 0000000000..a57128d562 --- /dev/null +++ b/src/__tests__/media-card-test.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import {MediaCard} from '../card'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; +import Tag from '../tag'; +import Stack from '../stack'; +import Image from '../image'; +import {Text2} from '../text'; + +test('MediaCard "href" label', async () => { + render( + + } + headline={Headline} + pretitle="Pretitle" + subtitle="Subtitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); +}); + +test('MediaCard "to" label', async () => { + render( + + } + headline={Headline} + pretitle="Pretitle" + subtitle="Subtitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); +}); + +test('MediaCard "onPress" label', async () => { + render( + + {}} + media={} + headline={Headline} + pretitle="Pretitle" + subtitle="Subtitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); +}); diff --git a/src/__tests__/naked-card-test.tsx b/src/__tests__/naked-card-test.tsx new file mode 100644 index 0000000000..ce8ce22384 --- /dev/null +++ b/src/__tests__/naked-card-test.tsx @@ -0,0 +1,153 @@ +import * as React from 'react'; +import {NakedCard, SmallNakedCard} from '../card'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; +import Tag from '../tag'; +import Stack from '../stack'; +import Image from '../image'; +import {Text2} from '../text'; + +test('NakedCard "href" label', async () => { + render( + + } + headline={Headline} + pretitle="Pretitle" + subtitle="Subtitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); +}); + +test('NakedCard "to" label', async () => { + render( + + } + headline={Headline} + pretitle="Pretitle" + subtitle="Subtitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); +}); + +test('NakedCard "onPress" label', async () => { + render( + + {}} + media={} + headline={Headline} + pretitle="Pretitle" + subtitle="Subtitle" + title="Title" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); +}); + +test('SmallNakedCard "href" label', async () => { + render( + + } + title="Title" + subtitle="Subtitle" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Subtitle Description Extra line 1Extra line 2'}); +}); + +test('SmallNakedCard "to" label', async () => { + render( + + } + title="Title" + subtitle="Subtitle" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Subtitle Description Extra line 1Extra line 2'}); +}); + +test('SmallNakedCard "onPress" label', async () => { + render( + + {}} + media={} + title="Title" + subtitle="Subtitle" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: 'Title Subtitle Description Extra line 1Extra line 2'}); +}); diff --git a/src/__tests__/poster-card-test.tsx b/src/__tests__/poster-card-test.tsx index 84da171f27..dc25739c5b 100644 --- a/src/__tests__/poster-card-test.tsx +++ b/src/__tests__/poster-card-test.tsx @@ -3,63 +3,83 @@ import {PosterCard} from '../card'; import {makeTheme} from './test-utils'; import {render, screen} from '@testing-library/react'; import ThemeContextProvider from '../theme-context-provider'; +import Stack from '../stack'; +import {Text2} from '../text'; test('PosterCard "href" label', async () => { render( + Extra line 1 + Extra line 2 + + } /> ); - expect( - await screen.findByRole('link', {name: 'Title Headline Pretitle Subtitle Description'}) - ).toBeInTheDocument(); + await screen.findByRole('link', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); }); test('PosterCard "to" label', async () => { render( + Extra line 1 + Extra line 2 + + } /> ); - expect( - await screen.findByRole('link', {name: 'Title Headline Pretitle Subtitle Description'}) - ).toBeInTheDocument(); + await screen.findByRole('link', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); }); test('PosterCard "onPress" label', async () => { render( {}} + isInverse headline="Headline" pretitle="Pretitle" title="Title" subtitle="Subtitle" description="Description" - onPress={() => {}} - isInverse + extra={ + + Extra line 1 + Extra line 2 + + } /> ); - expect( - await screen.findByRole('button', {name: 'Title Headline Pretitle Subtitle Description'}) - ).toBeInTheDocument(); + await screen.findByRole('button', { + name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', + }); }); diff --git a/src/__tests__/snap-card-test.tsx b/src/__tests__/snap-card-test.tsx new file mode 100644 index 0000000000..b0bba458c2 --- /dev/null +++ b/src/__tests__/snap-card-test.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import {SnapCard} from '../card'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; +import Stack from '../stack'; +import {Text2} from '../text'; + +test('SnapCard "href" label', async () => { + render( + + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Subtitle Description Extra line 1Extra line 2'}); +}); + +test('SnapCard "to" label', async () => { + render( + + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('link', {name: 'Title Subtitle Description Extra line 1Extra line 2'}); +}); + +test('SnapCard "onPress" label', async () => { + render( + + {}} + title="Title" + subtitle="Subtitle" + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: 'Title Subtitle Description Extra line 1Extra line 2'}); +}); diff --git a/src/card.tsx b/src/card.tsx index d6a3a4311e..0cfa41c17a 100644 --- a/src/card.tsx +++ b/src/card.tsx @@ -358,6 +358,7 @@ const useVideoWithControls = ( type CardContentProps = { headline?: string | RendersNullableElement; + headlineRef?: (instance: HTMLElement | null) => void; pretitle?: string; pretitleLinesMax?: number; title?: string; @@ -368,12 +369,14 @@ type CardContentProps = { description?: string; descriptionLinesMax?: number; extra?: React.ReactNode; + extraRef?: (instance: HTMLElement | null) => void; button?: RendersNullableElement; buttonLink?: RendersNullableElement; }; const CardContent: React.FC = ({ headline, + headlineRef, pretitle, pretitleLinesMax, title, @@ -384,19 +387,11 @@ const CardContent: React.FC = ({ description, descriptionLinesMax, extra, + extraRef, button, buttonLink, }) => { const {textPresets} = useTheme(); - const renderHeadline = () => { - if (!headline) { - return null; - } - if (typeof headline === 'string') { - return {headline}; - } - return headline; - }; return (
= ({ flexDirection: 'column', })} > -
- - {(headline || pretitle || title || subtitle) && ( -
- - {renderHeadline()} - - {pretitle && ( - - {pretitle} - - )} - - {title} - - - {subtitle} - - - -
- )} - - {description && ( + {/** using flex instead of nested Stacks, this way we can rearrange texts so the DOM structure makes more sense for screen reader users */} +
+ {title && ( +
+ + {title} + +
+ )} + {headline && ( + // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description +
+ {typeof headline === 'string' ? {headline} : headline} +
+ )} + {pretitle && ( +
+ + {pretitle} + +
+ )} + {subtitle && ( +
+ + {subtitle} + +
+ )} + {description && ( + // this is tricky, when headline exists, the 8px padding is added by it. + // Otherwise, only 4px are added by title|pretitle|subtitle, so we need to add 4px more +
= ({ > {description} - )} - - - {extra &&
{extra}
} +
+ )} + {extra &&
{extra}
}
{(button || buttonLink) && ( @@ -534,19 +541,25 @@ export const MediaCard = React.forwardRef( button, buttonLink, dataAttributes, - 'aria-label': ariaLabel, + 'aria-label': ariaLabelProp, onClose, ...touchableProps }, ref ) => { - const isTouchable = touchableProps.href || touchableProps.to || touchableProps.onPress; + const isTouchable = !!(touchableProps.href || touchableProps.to || touchableProps.onPress); + const {text: headlineText, ref: headlineRef} = useInnerText(); + const {text: extraText, ref: extraRef} = useInnerText(); + + const ariaLabel = + ariaLabelProp || + [title, headlineText, pretitle, subtitle, description, extraText].filter(Boolean).join(' '); return ( @@ -554,7 +567,7 @@ export const MediaCard = React.forwardRef( maybe {...touchableProps} className={styles.touchable} - aria-label={ariaLabel} + aria-label={isTouchable ? ariaLabel : undefined} > {isTouchable &&
}
@@ -564,6 +577,7 @@ export const MediaCard = React.forwardRef(
( description={description} descriptionLinesMax={descriptionLinesMax} extra={extra} + extraRef={extraRef} button={button} buttonLink={buttonLink} /> @@ -622,23 +637,34 @@ export const NakedCard = React.forwardRef( button, buttonLink, dataAttributes, - 'aria-label': ariaLabel, + 'aria-label': ariaLabelProp, onClose, ...touchableProps }, ref ) => { - const isTouchable = touchableProps.href || touchableProps.to || touchableProps.onPress; + const isTouchable = !!(touchableProps.href || touchableProps.to || touchableProps.onPress); const isCircularMedia = media && media.type === Image && (media.props as any).circular; + const {text: headlineText, ref: headlineRef} = useInnerText(); + const {text: extraText, ref: extraRef} = useInnerText(); + + const ariaLabel = + ariaLabelProp || + [title, headlineText, pretitle, subtitle, description, extraText].filter(Boolean).join(' '); return ( - +
{isTouchable && ( @@ -653,6 +679,7 @@ export const NakedCard = React.forwardRef(
( description={description} descriptionLinesMax={descriptionLinesMax} extra={extra} + extraRef={extraRef} button={button} buttonLink={buttonLink} /> @@ -717,23 +745,32 @@ export const SmallNakedCard = React.forwardRef { - const isTouchable = touchableProps.href || touchableProps.to || touchableProps.onPress; + const isTouchable = !!(touchableProps.href || touchableProps.to || touchableProps.onPress); const isCircularMedia = media && media.type === Image && (media.props as any).circular; const {textPresets} = useTheme(); + const {text: extraText, ref: extraRef} = useInnerText(); + + const ariaLabel = + ariaLabelProp || [title, subtitle, description, extraText].filter(Boolean).join(' '); return ( - +
{isTouchable && ( @@ -780,7 +817,7 @@ export const SmallNakedCard = React.forwardRef
- {extra &&
{extra}
} + {extra &&
{extra}
}
@@ -842,7 +879,7 @@ export const DataCard = React.forwardRef( button, buttonLink, dataAttributes, - 'aria-label': ariaLabel, + 'aria-label': ariaLabelProp, onClose, aspectRatio, ...touchableProps @@ -850,15 +887,21 @@ export const DataCard = React.forwardRef( ref ) => { const hasIconOrHeadline = !!icon || !!headline; - const isTouchable = touchableProps.href || touchableProps.to || touchableProps.onPress; + const isTouchable = !!(touchableProps.href || touchableProps.to || touchableProps.onPress); + const {text: headlineText, ref: headlineRef} = useInnerText(); + const {text: extraText, ref: extraRef} = useInnerText(); const finalActions = useTopActions(actions, onClose); + const ariaLabel = + ariaLabelProp || + [title, headlineText, pretitle, description, extraText].filter(Boolean).join(' '); + return ( @@ -867,7 +910,7 @@ export const DataCard = React.forwardRef( maybe {...touchableProps} className={styles.touchable} - aria-label={ariaLabel} + aria-label={isTouchable ? ariaLabel : undefined} > {isTouchable &&
}
@@ -885,6 +928,7 @@ export const DataCard = React.forwardRef( )} ( )} - {extra &&
{extra}
} + {extra &&
{extra}
} {(button || buttonLink) && (
@@ -953,7 +997,7 @@ export const SnapCard = React.forwardRef( description, descriptionLinesMax, dataAttributes, - 'aria-label': ariaLabel, + 'aria-label': ariaLabelProp, extra, isInverse = false, aspectRatio, @@ -962,8 +1006,12 @@ export const SnapCard = React.forwardRef( ref ) => { const {textPresets} = useTheme(); - const isTouchable = touchableProps.href || touchableProps.to || touchableProps.onPress; + const isTouchable = !!(touchableProps.href || touchableProps.to || touchableProps.onPress); const overlayStyle = isInverse ? styles.touchableCardOverlayInverse : styles.touchableCardOverlay; + const {text: extraText, ref: extraRef} = useInnerText(); + + const ariaLabel = + ariaLabelProp || [title, subtitle, description, extraText].filter(Boolean).join(' '); return ( ( ref={ref} className={styles.touchableContainer} aspectRatio={aspectRatio} + aria-label={isTouchable ? undefined : ariaLabelProp} > {isTouchable &&
}
@@ -1030,7 +1079,7 @@ export const SnapCard = React.forwardRef( )}
- {extra &&
{extra}
} + {extra &&
{extra}
}
@@ -1039,6 +1088,56 @@ export const SnapCard = React.forwardRef( } ); +interface DisplayCardContentProps { + title?: React.ReactNode; + headline?: React.ReactNode; + pretitle?: React.ReactNode; + subtitle?: React.ReactNode; + description?: React.ReactNode; + extra?: React.ReactNode; + headlineRef?: (instance: HTMLElement | null) => void; + extraRef?: (instance: HTMLElement | null) => void; +} + +const DisplayCardContent: React.FC = ({ + title, + headline, + pretitle, + subtitle, + description, + extra, + headlineRef, + extraRef, +}) => { + // using flex instead of nested Stacks, this way we can rearrange texts so the DOM structure makes more sense for screen reader users + return ( +
+ {title &&
{title}
} + {headline && ( + // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description +
+ {headline} +
+ )} + {pretitle &&
{pretitle}
} + + {subtitle &&
{subtitle}
} + {description && ( + // this is tricky, the padding between a headline and a description is 16px + // but the padding between a title|pretitle|subtitle and a description is 8px (4px + 4px) +
+ {description} +
+ )} + {extra &&
{extra}
} +
+ ); +}; + interface CommonDisplayCardProps { /** * Typically a mistica-icons component element @@ -1128,7 +1227,7 @@ const DisplayCard = React.forwardRef( width, height, aspectRatio, - 'aria-label': ariaLabel, + 'aria-label': ariaLabelProp, ...touchableProps }, ref @@ -1137,6 +1236,8 @@ const DisplayCard = React.forwardRef( const hasVideo = backgroundVideo !== undefined; const image = renderBackgroundImage(backgroundImage); const {video, videoAction} = useVideoWithControls(backgroundVideo, poster, backgroundVideoRef); + const {text: headlineText, ref: headlineRef} = useInnerText(); + const {text: extraText, ref: extraRef} = useInnerText(); if (hasVideo) { actions = videoAction ? [videoAction] : []; @@ -1147,7 +1248,7 @@ const DisplayCard = React.forwardRef( const textShadow = withGradient ? '0 0 16px rgba(0,0,0,0.4)' : undefined; const hasTopActions = actions?.length || onClose; - const isTouchable = touchableProps.href || touchableProps.to || touchableProps.onPress; + const isTouchable = !!(touchableProps.href || touchableProps.to || touchableProps.onPress); const overlayStyle = hasImage || hasVideo ? styles.touchableCardOverlayMedia @@ -1155,6 +1256,10 @@ const DisplayCard = React.forwardRef( ? styles.touchableCardOverlayInverse : styles.touchableCardOverlay; + const ariaLabel = + ariaLabelProp || + [title, headlineText, pretitle, description, extraText].filter(Boolean).join(' '); + return ( ( width={width} height={height} aspectRatio={aspectRatio} - aria-label={ariaLabel} + aria-label={isTouchable ? undefined : ariaLabelProp} className={styles.touchableContainer} > ( maybe {...touchableProps} className={styles.touchable} - aria-label={ariaLabel} + aria-label={isTouchable ? ariaLabel : undefined} > {isTouchable &&
} @@ -1226,39 +1331,36 @@ const DisplayCard = React.forwardRef( className={withGradient ? styles.displayCardGradient : undefined} > -
- - {(headline || pretitle || title) && ( -
- - {headline} - - {pretitle && ( - - {pretitle} - - )} - - {title} - - - -
- )} - - {description && ( + + {title} + + ) : undefined + } + headline={headline} + pretitle={ + pretitle ? ( + + {pretitle} + + ) : undefined + } + description={ + description ? ( ( > {description} - )} -
- {extra} -
+ ) : undefined + } + extra={extra} + headlineRef={headlineRef} + extraRef={extraRef} + /> + {(button || secondaryButton || buttonLink) && ( ( const image = renderBackgroundImage(backgroundImage); const {video, videoAction} = useVideoWithControls(backgroundVideo, poster, backgroundVideoRef); const {text: headlineText, ref: headlineRef} = useInnerText(); + const {text: extraText, ref: extraRef} = useInnerText(); if (hasVideo) { actions = videoAction ? [videoAction] : []; @@ -1446,7 +1552,8 @@ export const PosterCard = React.forwardRef( : styles.touchableCardOverlay; const ariaLabel = - ariaLabelProp || [title, headlineText, pretitle, subtitle, description].filter(Boolean).join(' '); + ariaLabelProp || + [title, headlineText, pretitle, subtitle, description, extraText].filter(Boolean).join(' '); return ( ( paddingBottom={24} className={withGradient ? styles.displayCardGradient : undefined} > - {/* using flex instead of nested Stacks, this way we can rearrange texts so the DOM structure makes more sense for screen reader users */} -
- {title && ( -
+ ( > {title} -
- )} - {headline && ( - // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description -
- {headline} -
- )} - {pretitle && ( -
+ ) : undefined + } + headline={headline} + pretitle={ + pretitle ? ( {pretitle} -
- )} - - {subtitle && ( -
+ ) : undefined + } + subtitle={ + subtitle ? ( ( > {subtitle} -
- )} - {description && ( - // this is tricky, the padding between a headline and a description is 16px - // but the padding between a title|pretitle|subtitle and a description is 8px (4px + 4px) -
+ ) : undefined + } + description={ + description ? ( ( > {description} -
- )} - {extra} -
+ ) : undefined + } + headlineRef={headlineRef} + extra={extra} + extraRef={extraRef} + />