From cdcf2fcafcd4df435f38927cbbc65d52c9b29590 Mon Sep 17 00:00:00 2001 From: Marcos Kolodny Date: Tue, 26 Nov 2024 10:53:24 +0100 Subject: [PATCH] feat(Header, Cards, Hero, CoverHero): improve a11y (#1292) Issues: - [WEB-2087](https://jira.tid.es/browse/WEB-2087) - [WEB-2107](https://jira.tid.es/browse/WEB-2107) --- src/__stories__/cover-hero-story.tsx | 17 ++ src/__stories__/data-card-story.tsx | 17 ++ src/__stories__/display-data-card-story.tsx | 17 ++ src/__stories__/display-media-card-story.tsx | 17 ++ src/__stories__/hero-story.tsx | 17 ++ src/__stories__/media-card-story.tsx | 17 ++ src/__stories__/naked-card-story.tsx | 17 ++ src/__stories__/poster-card-story.tsx | 17 ++ src/__tests__/advanced-data-card-test.tsx | 124 +++++++++ src/__tests__/cover-hero-test.tsx | 44 ++++ src/__tests__/data-card-test.tsx | 153 ++++++----- src/__tests__/display-data-card-test.tsx | 153 ++++++----- src/__tests__/display-media-card-test.tsx | 159 +++++++----- src/__tests__/header-test.tsx | 42 +++ src/__tests__/hero-test.tsx | 44 ++++ src/__tests__/media-card-test.tsx | 168 +++++++----- src/__tests__/naked-card-test.tsx | 168 +++++++----- src/__tests__/poster-card-test.tsx | 168 +++++++----- src/card.css.ts | 2 +- src/card.tsx | 245 +++++++++++++----- .../__stories__/advanced-data-card-story.tsx | 17 ++ src/community/advanced-data-card.css.ts | 5 + src/community/advanced-data-card.tsx | 131 +++++++--- src/cover-hero.css.ts | 5 + src/cover-hero.tsx | 92 ++++--- src/header.css.ts | 5 + src/header.tsx | 115 +++++--- src/hero.css.ts | 5 + src/hero.tsx | 64 ++++- src/utils/headings.tsx | 15 ++ 30 files changed, 1485 insertions(+), 575 deletions(-) create mode 100644 src/__tests__/advanced-data-card-test.tsx create mode 100644 src/__tests__/cover-hero-test.tsx create mode 100644 src/__tests__/header-test.tsx create mode 100644 src/__tests__/hero-test.tsx create mode 100644 src/utils/headings.tsx diff --git a/src/__stories__/cover-hero-story.tsx b/src/__stories__/cover-hero-story.tsx index 5a70572d78..4472f9e2c2 100644 --- a/src/__stories__/cover-hero-story.tsx +++ b/src/__stories__/cover-hero-story.tsx @@ -13,6 +13,7 @@ import usingVrImg from './images/using-vr.jpg'; import beachImg from './images/beach.jpg'; import beachVideo from './videos/beach.mp4'; +import type {HeadingType} from '../utils/types'; import type {TagType} from '..'; export default { @@ -30,7 +31,9 @@ type Args = { headlineType: TagType; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; description: string; extra: boolean; sideExtra: boolean; @@ -56,7 +59,9 @@ export const Default: StoryComponent = ({ headlineType, headline, pretitle, + pretitleAs, title, + titleAs, description, extra, sideExtra, @@ -92,7 +97,9 @@ export const Default: StoryComponent = ({ dataAttributes={{testid: 'cover-hero'}} headline={headline ? {headline} : undefined} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} description={description} extra={extra ? : undefined} sideExtra={sideExtra ? : undefined} @@ -118,7 +125,9 @@ Default.args = { headlineType: 'promo', headline: 'Hero', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h1', description: 'This is a long description with a long text to see how this works', extra: false, sideExtra: false, @@ -197,6 +206,14 @@ Default.argTypes = { }, }, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; export const CoverHeroInSlideshow: StoryComponent = () => ( diff --git a/src/__stories__/data-card-story.tsx b/src/__stories__/data-card-story.tsx index d0602c45ef..aeced7f28c 100644 --- a/src/__stories__/data-card-story.tsx +++ b/src/__stories__/data-card-story.tsx @@ -17,6 +17,7 @@ import { import {Placeholder} from '../placeholder'; import avatarImg from './images/avatar.jpg'; +import type {HeadingType} from '../utils/types'; import type {AspectRatio} from '../card'; import type {TagType} from '..'; @@ -29,7 +30,9 @@ type DataCardArgs = { headlineType: TagType; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; subtitle: string; description: string; ariaLabel: string; @@ -47,7 +50,9 @@ export const Default: StoryComponent = ({ headline, headlineType, pretitle, + pretitleAs, title, + titleAs, subtitle, description, ariaLabel, @@ -99,7 +104,9 @@ export const Default: StoryComponent = ({ asset={assetElement} headline={headline && {headline}} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} subtitle={subtitle} description={description} extra={extra ? : undefined} @@ -142,7 +149,9 @@ Default.args = { headlineType: 'promo', headline: 'Priority', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h3', subtitle: 'Subtitle', description: 'This is a description for the card', extra: false, @@ -177,6 +186,14 @@ Default.argTypes = { }, }, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; export const Group: StoryComponent = () => { diff --git a/src/__stories__/display-data-card-story.tsx b/src/__stories__/display-data-card-story.tsx index 8cd8a30ab3..bb44a1b516 100644 --- a/src/__stories__/display-data-card-story.tsx +++ b/src/__stories__/display-data-card-story.tsx @@ -20,6 +20,7 @@ import { import {Placeholder} from '../placeholder'; import avatarImg from './images/avatar.jpg'; +import type {HeadingType} from '../utils/types'; import type {AspectRatio} from '../card'; import type {TagType} from '..'; @@ -32,7 +33,9 @@ type DisplayDataCardArgs = { headlineType: TagType; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; description: string; extra: boolean; closable: boolean; @@ -57,7 +60,9 @@ export const Default: StoryComponent = ({ headline, headlineType, pretitle, + pretitleAs, title, + titleAs, description, extra, actions = 'button', @@ -141,7 +146,9 @@ export const Default: StoryComponent = ({ asset={assetElement} headline={headline ? {headline} : undefined} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} description={description} aspectRatio={aspectRatioValue as AspectRatio} extra={extra ? : undefined} @@ -158,7 +165,9 @@ Default.args = { headlineType: 'promo', headline: 'Priority', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h3', description: 'This is a description for the card', extra: false, actions: 'button', @@ -201,6 +210,14 @@ Default.argTypes = { }, }, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; export const Group: StoryComponent = () => { diff --git a/src/__stories__/display-media-card-story.tsx b/src/__stories__/display-media-card-story.tsx index 42e6ae96f4..554717bf7a 100644 --- a/src/__stories__/display-media-card-story.tsx +++ b/src/__stories__/display-media-card-story.tsx @@ -23,6 +23,7 @@ import avatarImg from './images/avatar.jpg'; import beachVideo from './videos/beach.mp4'; import beachImg from './images/beach.jpg'; +import type {HeadingType} from '../utils/types'; import type {TagType} from '..'; export default { @@ -39,7 +40,9 @@ type DisplayMediaCardArgs = { background: 'image' | 'video'; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; description: string; extra: boolean; closable: boolean; @@ -65,7 +68,9 @@ export const Default: StoryComponent = ({ headlineType, background, pretitle, + pretitleAs, title, + titleAs, description, extra, actions = 'button', @@ -153,7 +158,9 @@ export const Default: StoryComponent = ({ asset={assetElement} headline={headline ? {headline} : undefined} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} description={description} {...interactiveActions} aria-label="Display media card label" @@ -174,7 +181,9 @@ Default.args = { background: 'image', headline: 'Priority', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h3', description: 'This is a description for the card', extra: false, actions: 'button', @@ -215,6 +224,14 @@ Default.argTypes = { options: ['1:1', '16:9', '7:10', '9:10', 'auto'], control: {type: 'select'}, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; Default.parameters = {fullScreen: true}; diff --git a/src/__stories__/hero-story.tsx b/src/__stories__/hero-story.tsx index 05b72ecc14..1cad9490ae 100644 --- a/src/__stories__/hero-story.tsx +++ b/src/__stories__/hero-story.tsx @@ -4,6 +4,7 @@ import usingVrImg from './images/using-vr.jpg'; import beachImg from './images/beach.jpg'; import beachVideo from './videos/beach.mp4'; +import type {HeadingType} from '../utils/types'; import type {TagType} from '..'; import type {AspectRatio} from '../video'; @@ -19,7 +20,9 @@ type HeroArgs = { headlineType: TagType; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; description: string; extra: boolean; actions: 'button' | 'link' | 'button and link'; @@ -36,7 +39,9 @@ export const Default: StoryComponent = ({ headline, headlineType, pretitle, + pretitleAs, title, + titleAs, description, actions, desktopMediaPosition, @@ -65,7 +70,9 @@ export const Default: StoryComponent = ({ media={mediaComponent} headline={headline ? {headline} : undefined} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} description={description} extra={extra ? : undefined} button={button} @@ -85,7 +92,9 @@ Default.args = { headlineType: 'promo', headline: 'Hero', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h1', description: 'This is a long description with a long text to see how this works', extra: false, actions: 'button and link', @@ -128,4 +137,12 @@ Default.argTypes = { }, }, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; diff --git a/src/__stories__/media-card-story.tsx b/src/__stories__/media-card-story.tsx index 6fa815fe41..745abc95b7 100644 --- a/src/__stories__/media-card-story.tsx +++ b/src/__stories__/media-card-story.tsx @@ -21,6 +21,7 @@ import tennisImg from './images/tennis.jpg'; import beachVideo from './videos/beach.mp4'; import avatarImg from './images/avatar.jpg'; +import type {HeadingType} from '../utils/types'; import type {TagType} from '..'; export default { @@ -36,7 +37,9 @@ type Args = { headlineType: TagType; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; subtitle: string; description: string; extra: boolean; @@ -50,7 +53,9 @@ export const Default: StoryComponent = ({ headline, headlineType, pretitle, + pretitleAs, title, + titleAs, subtitle, description, actions = 'button', @@ -99,7 +104,9 @@ export const Default: StoryComponent = ({ dataAttributes={{testid: 'media-card'}} headline={headline && {headline}} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} subtitle={subtitle} description={description} asset={assetElement} @@ -153,7 +160,9 @@ Default.args = { headlineType: 'promo', headline: 'Priority', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h3', subtitle: 'Subtitle', description: 'This is a description for the card', extra: false, @@ -179,6 +188,14 @@ Default.argTypes = { options: ['button', 'link', 'button and link', 'onPress', 'href', 'to', 'none'], control: {type: 'select'}, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; export const Group: StoryComponent = () => { diff --git a/src/__stories__/naked-card-story.tsx b/src/__stories__/naked-card-story.tsx index cac8979fd8..e6b6d8c4da 100644 --- a/src/__stories__/naked-card-story.tsx +++ b/src/__stories__/naked-card-story.tsx @@ -23,6 +23,7 @@ import beachVideo from './videos/beach.mp4'; import {SmallNakedCard} from '../card'; import avatarImg from './images/avatar.jpg'; +import type {HeadingType} from '../utils/types'; import type {TagType} from '..'; export default { @@ -41,7 +42,9 @@ type Args = { headlineType: TagType; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; subtitle: string; description: string; extra: boolean; @@ -55,7 +58,9 @@ export const Default: StoryComponent = ({ headline, headlineType, pretitle, + pretitleAs, title, + titleAs, subtitle, description, actions = 'button', @@ -106,7 +111,9 @@ export const Default: StoryComponent = ({ dataAttributes={{testid: 'naked-card'}} headline={headline && {headline}} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} subtitle={subtitle} description={description} media={ @@ -160,7 +167,9 @@ Default.args = { headlineType: 'promo', headline: 'Priority', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h3', subtitle: 'Subtitle', description: 'This is a description for the card', extra: false, @@ -186,6 +195,14 @@ Default.argTypes = { options: ['button', 'link', 'button and link', 'onPress', 'href', 'to', 'none'], control: {type: 'select'}, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; type SmallArgs = { diff --git a/src/__stories__/poster-card-story.tsx b/src/__stories__/poster-card-story.tsx index d134708eff..238a44c0f6 100644 --- a/src/__stories__/poster-card-story.tsx +++ b/src/__stories__/poster-card-story.tsx @@ -20,6 +20,7 @@ import avatarImg from './images/avatar.jpg'; import beachVideo from './videos/beach.mp4'; import beachImg from './images/beach.jpg'; +import type {HeadingType} from '../utils/types'; import type {TagType} from '..'; export default { @@ -39,7 +40,9 @@ type PosterCardArgs = { headlineType: TagType; headline: string; pretitle: string; + pretitleAs: HeadingType; title: string; + titleAs: HeadingType; subtitle: string; description: string; extra: boolean; @@ -63,7 +66,9 @@ export const Default: StoryComponent = ({ headline, headlineType, pretitle, + pretitleAs, title, + titleAs, subtitle, description, extra, @@ -144,7 +149,9 @@ export const Default: StoryComponent = ({ asset={assetElement} headline={headline ? {headline} : undefined} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} subtitle={subtitle} description={description} extra={extra ? : undefined} @@ -169,7 +176,9 @@ Default.args = { variant: 'default', headline: 'Priority', pretitle: 'Pretitle', + pretitleAs: 'span', title: 'Title', + titleAs: 'h3', subtitle: 'Subtitle', description: 'This is a description for the card', extra: false, @@ -226,6 +235,14 @@ Default.argTypes = { options: ['onPress', 'href', 'to', 'none'], control: {type: 'select'}, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; Default.parameters = {fullScreen: true}; diff --git a/src/__tests__/advanced-data-card-test.tsx b/src/__tests__/advanced-data-card-test.tsx new file mode 100644 index 0000000000..3a3f81989a --- /dev/null +++ b/src/__tests__/advanced-data-card-test.tsx @@ -0,0 +1,124 @@ +import * as React from 'react'; +import {AdvancedDataCard} from '../community'; +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'; + +const titleFirst = + 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2Extra line 3Extra line 4'; +const pretitleFirst = + 'Pretitle Headline Title Subtitle Description Extra line 1Extra line 2Extra line 3Extra line 4'; + +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'AdvancedDataCard "href" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + titleAs={titleAs} + subtitle="Subtitle" + title="Title" + description="Description" + extra={[ + + Extra line 1 + Extra line 2 + , + + Extra line 3 + Extra line 4 + , + ]} + /> + + ); + + await screen.findByRole('link', {name: expectedLabel}); + } +); + +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'AdvancedDataCard "to" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + titleAs={titleAs} + subtitle="Subtitle" + title="Title" + description="Description" + extra={[ + + Extra line 1 + Extra line 2 + , + + Extra line 3 + Extra line 4 + , + ]} + /> + + ); + + await screen.findByRole('link', {name: expectedLabel}); + } +); + +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'AdvancedDataCard "onPress" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + {}} + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + titleAs={titleAs} + subtitle="Subtitle" + title="Title" + description="Description" + extra={[ + + Extra line 1 + Extra line 2 + , + + Extra line 3 + Extra line 4 + , + ]} + /> + + ); + + await screen.findByRole('button', {name: expectedLabel}); + } +); diff --git a/src/__tests__/cover-hero-test.tsx b/src/__tests__/cover-hero-test.tsx new file mode 100644 index 0000000000..bce8fc7f01 --- /dev/null +++ b/src/__tests__/cover-hero-test.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import CoverHero from '../cover-hero'; +import Tag from '../tag'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; + +const pretitleFirst = 'PretitleHeadlineTitleDescription'; +const titleFirst = 'TitleHeadlinePretitleDescription'; + +test.each` + pretitleAs | titleAs | expectedOrder + ${undefined} | ${undefined} | ${titleFirst} + ${undefined} | ${'span'} | ${titleFirst} + ${undefined} | ${'h1'} | ${titleFirst} + ${'span'} | ${undefined} | ${titleFirst} + ${'h3'} | ${undefined} | ${titleFirst} + ${'h3'} | ${'h1'} | ${titleFirst} + ${'span'} | ${'h1'} | ${titleFirst} + ${'h1'} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h3'} | ${pretitleFirst} + ${'h1'} | ${'span'} | ${pretitleFirst} +`( + 'CoverHero has correct reading order with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedOrder}) => { + render( + + Headline} + pretitle="Pretitle" + title="Title" + description="Description" + titleAs={titleAs} + pretitleAs={pretitleAs} + dataAttributes={{testid: 'cover-hero'}} + /> + + ); + + const coverHero = await screen.findByTestId('cover-hero'); + expect(coverHero.textContent).toEqual(expectedOrder); + } +); diff --git a/src/__tests__/data-card-test.tsx b/src/__tests__/data-card-test.tsx index 2056c341b0..4b4dc2cf17 100644 --- a/src/__tests__/data-card-test.tsx +++ b/src/__tests__/data-card-test.tsx @@ -8,71 +8,104 @@ import Stack from '../stack'; import {Text2} from '../text'; import userEvent from '@testing-library/user-event'; -test('DataCard "href" label', async () => { - render( - - Headline} - pretitle="Pretitle" - title="Title" - description="Description" - extra={ - - Extra line 1 - Extra line 2 - - } - /> - - ); +const titleFirst = 'Title Headline Pretitle Description Extra line 1Extra line 2'; +const pretitleFirst = 'Pretitle Headline Title Description Extra line 1Extra line 2'; - await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DataCard "href" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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: expectedLabel}); + } +); - await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DataCard "to" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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('link', {name: expectedLabel}); + } +); - await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DataCard "onPress" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + {}} + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: expectedLabel}); + } +); test('DataCard onClose custom label', async () => { const closeSpy = jest.fn(); diff --git a/src/__tests__/display-data-card-test.tsx b/src/__tests__/display-data-card-test.tsx index 49e33ffc06..e0420f5df4 100644 --- a/src/__tests__/display-data-card-test.tsx +++ b/src/__tests__/display-data-card-test.tsx @@ -8,71 +8,104 @@ import Stack from '../stack'; import {Text2} from '../text'; import userEvent from '@testing-library/user-event'; -test('DisplayDataCard "href" label', async () => { - render( - - Headline} - pretitle="Pretitle" - title="Title" - description="Description" - extra={ - - Extra line 1 - Extra line 2 - - } - /> - - ); +const titleFirst = 'Title Headline Pretitle Description Extra line 1Extra line 2'; +const pretitleFirst = 'Pretitle Headline Title Description Extra line 1Extra line 2'; - await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DisplayDataCard "href" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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: expectedLabel}); + } +); - await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DisplayDataCard "to" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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('link', {name: expectedLabel}); + } +); - await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DisplayDataCard "onPress" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + {}} + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: expectedLabel}); + } +); test('DisplayDataCard onClose custom label', async () => { const closeSpy = jest.fn(); diff --git a/src/__tests__/display-media-card-test.tsx b/src/__tests__/display-media-card-test.tsx index da8d978d22..24db7b9898 100644 --- a/src/__tests__/display-media-card-test.tsx +++ b/src/__tests__/display-media-card-test.tsx @@ -8,74 +8,107 @@ import Stack from '../stack'; import {Text2} from '../text'; import userEvent from '@testing-library/user-event'; -test('DisplayMediaCard "href" label', async () => { - render( - - Headline} - pretitle="Pretitle" - title="Title" - description="Description" - extra={ - - Extra line 1 - Extra line 2 - - } - /> - - ); +const titleFirst = 'Title Headline Pretitle Description Extra line 1Extra line 2'; +const pretitleFirst = 'Pretitle Headline Title Description Extra line 1Extra line 2'; - await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DisplayMediaCard "href" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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: expectedLabel}); + } +); - await screen.findByRole('link', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DisplayMediaCard "to" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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('link', {name: expectedLabel}); + } +); - await screen.findByRole('button', {name: 'Title Headline Pretitle Description Extra line 1Extra line 2'}); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'DisplayMediaCard "onPress" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + {}} + backgroundImage="https://source.unsplash.com/900x900/" + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: expectedLabel}); + } +); test('DisplayMediaCard onClose custom label', async () => { const closeSpy = jest.fn(); diff --git a/src/__tests__/header-test.tsx b/src/__tests__/header-test.tsx new file mode 100644 index 0000000000..31de0a8f68 --- /dev/null +++ b/src/__tests__/header-test.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import {Header} from '../header'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; + +const pretitleFirst = 'PretitleHeadlineTitleDescription'; +const titleFirst = 'TitleHeadlinePretitleDescription'; + +test.each` + pretitleAs | titleAs | expectedOrder + ${undefined} | ${undefined} | ${titleFirst} + ${undefined} | ${'span'} | ${titleFirst} + ${undefined} | ${'h1'} | ${titleFirst} + ${'span'} | ${undefined} | ${titleFirst} + ${'h3'} | ${undefined} | ${titleFirst} + ${'h3'} | ${'h1'} | ${titleFirst} + ${'span'} | ${'h1'} | ${titleFirst} + ${'h1'} | ${undefined} | ${pretitleFirst} + ${'h1'} | ${'h3'} | ${pretitleFirst} + ${'h1'} | ${'span'} | ${pretitleFirst} +`( + 'Header has correct reading order with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedOrder}) => { + render( + +
+ + ); + + const header = await screen.findByTestId('header'); + expect(header.textContent).toEqual(expectedOrder); + } +); diff --git a/src/__tests__/hero-test.tsx b/src/__tests__/hero-test.tsx new file mode 100644 index 0000000000..c93a08832a --- /dev/null +++ b/src/__tests__/hero-test.tsx @@ -0,0 +1,44 @@ +import * as React from 'react'; +import Hero from '../hero'; +import Tag from '../tag'; +import {makeTheme} from './test-utils'; +import {render, screen} from '@testing-library/react'; +import ThemeContextProvider from '../theme-context-provider'; + +const pretitleFirst = 'PretitleHeadlineTitleDescription'; +const titleFirst = 'TitleHeadlinePretitleDescription'; + +test.each` + pretitleAs | titleAs | expectedOrder + ${undefined} | ${undefined} | ${titleFirst} + ${undefined} | ${'span'} | ${titleFirst} + ${undefined} | ${'h1'} | ${titleFirst} + ${'span'} | ${undefined} | ${titleFirst} + ${'h3'} | ${undefined} | ${titleFirst} + ${'h3'} | ${'h1'} | ${titleFirst} + ${'span'} | ${'h1'} | ${titleFirst} + ${'h1'} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h3'} | ${pretitleFirst} + ${'h1'} | ${'span'} | ${pretitleFirst} +`( + 'Hero has correct reading order with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedOrder}) => { + render( + + } + headline={Headline} + pretitle="Pretitle" + title="Title" + description="Description" + titleAs={titleAs} + pretitleAs={pretitleAs} + dataAttributes={{testid: 'hero'}} + /> + + ); + + const hero = await screen.findByTestId('hero'); + expect(hero.textContent).toEqual(expectedOrder); + } +); diff --git a/src/__tests__/media-card-test.tsx b/src/__tests__/media-card-test.tsx index 35776af9de..710d23fe10 100644 --- a/src/__tests__/media-card-test.tsx +++ b/src/__tests__/media-card-test.tsx @@ -9,83 +9,107 @@ import Image from '../image'; import {Text2} from '../text'; import userEvent from '@testing-library/user-event'; -test('MediaCard "href" label', async () => { - render( - - } - headline={Headline} - pretitle="Pretitle" - subtitle="Subtitle" - title="Title" - description="Description" - extra={ - - Extra line 1 - Extra line 2 - - } - /> - - ); +const titleFirst = 'Title Headline Pretitle Description Extra line 1Extra line 2'; +const pretitleFirst = 'Pretitle Headline Title Description Extra line 1Extra line 2'; - await screen.findByRole('link', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'MediaCard "href" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + } + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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: expectedLabel}); + } +); - await screen.findByRole('link', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'MediaCard "to" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + } + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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('link', {name: expectedLabel}); + } +); - await screen.findByRole('button', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'MediaCard "onPress" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + {}} + media={} + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: expectedLabel}); + } +); test('MediaCard onClose custom label', async () => { const closeSpy = jest.fn(); diff --git a/src/__tests__/naked-card-test.tsx b/src/__tests__/naked-card-test.tsx index 4cdce0b345..d68672fa09 100644 --- a/src/__tests__/naked-card-test.tsx +++ b/src/__tests__/naked-card-test.tsx @@ -9,83 +9,107 @@ import Image from '../image'; import {Text2} from '../text'; import userEvent from '@testing-library/user-event'; -test('NakedCard "href" label', async () => { - render( - - } - headline={Headline} - pretitle="Pretitle" - subtitle="Subtitle" - title="Title" - description="Description" - extra={ - - Extra line 1 - Extra line 2 - - } - /> - - ); +const titleFirst = 'Title Headline Pretitle Description Extra line 1Extra line 2'; +const pretitleFirst = 'Pretitle Headline Title Description Extra line 1Extra line 2'; - await screen.findByRole('link', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'NakedCard "href" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + } + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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: expectedLabel}); + } +); - await screen.findByRole('link', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'NakedCard "to" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + } + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra 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('link', {name: expectedLabel}); + } +); - await screen.findByRole('button', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'NakedCard "onPress" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + {}} + media={} + headline={Headline} + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: expectedLabel}); + } +); test('NakedCard onClose custom label', async () => { const closeSpy = jest.fn(); diff --git a/src/__tests__/poster-card-test.tsx b/src/__tests__/poster-card-test.tsx index bca1d5ef10..af50daef48 100644 --- a/src/__tests__/poster-card-test.tsx +++ b/src/__tests__/poster-card-test.tsx @@ -7,83 +7,107 @@ import Stack from '../stack'; import {Text2} from '../text'; import userEvent from '@testing-library/user-event'; -test('PosterCard "href" label', async () => { - render( - - - Extra line 1 - Extra line 2 - - } - /> - - ); +const titleFirst = 'Title Headline Pretitle Description Extra line 1Extra line 2'; +const pretitleFirst = 'Pretitle Headline Title Description Extra line 1Extra line 2'; - await screen.findByRole('link', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'PosterCard "href" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + + Extra line 1 + Extra line 2 + + } + /> + + ); -test('PosterCard "to" label', async () => { - render( - - - Extra line 1 - Extra line 2 - - } - /> - - ); + await screen.findByRole('link', {name: expectedLabel}); + } +); - await screen.findByRole('link', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'PosterCard "to" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + + Extra line 1 + Extra line 2 + + } + /> + + ); -test('PosterCard "onPress" label', async () => { - render( - - {}} - isInverse - headline="Headline" - pretitle="Pretitle" - title="Title" - subtitle="Subtitle" - description="Description" - extra={ - - Extra line 1 - Extra line 2 - - } - /> - - ); + await screen.findByRole('link', {name: expectedLabel}); + } +); - await screen.findByRole('button', { - name: 'Title Headline Pretitle Subtitle Description Extra line 1Extra line 2', - }); -}); +test.each` + pretitleAs | titleAs | expectedLabel + ${undefined} | ${undefined} | ${titleFirst} + ${'h1'} | ${'h2'} | ${pretitleFirst} + ${'h2'} | ${'h1'} | ${titleFirst} +`( + 'PosterCard "onPress" label with pretitleAs={$pretitleAs} and titleAs={$titleAs}', + async ({pretitleAs, titleAs, expectedLabel}) => { + render( + + {}} + isInverse + headline="Headline" + pretitle="Pretitle" + pretitleAs={pretitleAs} + title="Title" + titleAs={titleAs} + description="Description" + extra={ + + Extra line 1 + Extra line 2 + + } + /> + + ); + + await screen.findByRole('button', {name: expectedLabel}); + } +); test('PosterCard onClose custom label', async () => { const closeSpy = jest.fn(); diff --git a/src/card.css.ts b/src/card.css.ts index 6613731128..97bf5e2ca7 100644 --- a/src/card.css.ts +++ b/src/card.css.ts @@ -415,7 +415,7 @@ export const dataCardTopActionsWithoutIcon = style({ }, }); -export const flexColumn = style({ +export const flexColumn = sprinkles({ display: 'flex', flexDirection: 'column', }); diff --git a/src/card.tsx b/src/card.tsx index c0c50a78ea..5c2f570dfe 100644 --- a/src/card.tsx +++ b/src/card.tsx @@ -27,22 +27,23 @@ import {getPrefixedDataAttributes} from './utils/dom'; import {isRunningAcceptanceTest} from './utils/platform'; import {applyCssVars} from './utils/css'; import * as tokens from './text-tokens'; +import { + type DataAttributes, + type HeadingType, + type IconProps, + type RendersElement, + type RendersNullableElement, + type TrackingEvent, +} from './utils/types'; +import {isBiggerHeading} from './utils/headings'; import type {Variant} from './theme-variant-context'; import type {PressHandler} from './touchable'; import type {VideoElement, VideoSource} from './video'; import type {ButtonLink, ButtonPrimary, ButtonSecondary} from './button'; import type {ExclusifyUnion} from './utils/utility-types'; -import type { - DataAttributes, - HeadingType, - IconProps, - RendersElement, - RendersNullableElement, - TrackingEvent, -} from './utils/types'; -const useInnerText = () => { +export const useInnerText = (): {text: string; ref: (instance: HTMLElement | null) => void} => { const [text, setText] = React.useState(''); const ref: React.LegacyRef = React.useCallback((node: HTMLElement) => { @@ -369,6 +370,7 @@ type CardContentProps = { headline?: string | RendersNullableElement; headlineRef?: (instance: HTMLElement | null) => void; pretitle?: string; + pretitleAs?: HeadingType; pretitleLinesMax?: number; title?: string; titleAs?: HeadingType; @@ -387,6 +389,7 @@ const CardContent = ({ headline, headlineRef, pretitle, + pretitleAs, pretitleLinesMax, title, titleAs = 'h3', @@ -405,31 +408,81 @@ const CardContent = ({
{/** 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} - -
+ {isBiggerHeading(titleAs, pretitleAs) ? ( + <> + {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} + +
+ )} + + ) : ( + <> + <> + {pretitle && ( +
+ + {pretitle} + +
+ )} + {headline && ( + // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description +
+ {typeof headline === 'string' ? ( + {headline} + ) : ( + headline + )} +
+ )} + {title && ( +
+ + {title} + +
+ )} + + )} {subtitle && (
@@ -502,6 +555,7 @@ interface MediaCardBaseProps { asset?: React.ReactElement; headline?: string | RendersNullableElement; pretitle?: string; + pretitleAs?: HeadingType; pretitleLinesMax?: number; title?: string; titleAs?: HeadingType; @@ -535,6 +589,7 @@ export const MediaCard = React.forwardRef( asset, headline, pretitle, + pretitleAs, pretitleLinesMax, subtitle, subtitleLinesMax, @@ -561,7 +616,12 @@ export const MediaCard = React.forwardRef( const ariaLabel = ariaLabelProp || - [title, headlineText, pretitle, subtitle, description, extraText].filter(Boolean).join(' '); + (isBiggerHeading(titleAs, pretitleAs) + ? [title, headlineText, pretitle, subtitle, description, extraText] + : [pretitle, headlineText, title, subtitle, description, extraText] + ) + .filter(Boolean) + .join(' '); return ( ( aria-label={isTouchable ? ariaLabel : undefined} > {isTouchable &&
} -
+
{media}
@@ -587,6 +647,7 @@ export const MediaCard = React.forwardRef( headline={headline} headlineRef={headlineRef} pretitle={pretitle} + pretitleAs={pretitleAs} pretitleLinesMax={pretitleLinesMax} title={title} titleAs={titleAs} @@ -633,6 +694,7 @@ export const NakedCard = React.forwardRef( asset, headline, pretitle, + pretitleAs, pretitleLinesMax, subtitle, subtitleLinesMax, @@ -660,7 +722,12 @@ export const NakedCard = React.forwardRef( const ariaLabel = ariaLabelProp || - [title, headlineText, pretitle, subtitle, description, extraText].filter(Boolean).join(' '); + (isBiggerHeading(titleAs, pretitleAs) + ? [title, headlineText, pretitle, subtitle, description, extraText] + : [pretitle, headlineText, title, subtitle, description, extraText] + ) + .filter(Boolean) + .join(' '); return ( ( className={styles.touchable} aria-label={isTouchable ? ariaLabel : undefined} > -
+
{isTouchable && (
( headline={headline} headlineRef={headlineRef} pretitle={pretitle} + pretitleAs={pretitleAs} pretitleLinesMax={pretitleLinesMax} title={title} titleAs={titleAs} @@ -786,7 +854,7 @@ export const SmallNakedCard = React.forwardRef -
+
{isTouchable && (
; pretitle?: string; + pretitleAs?: HeadingType; pretitleLinesMax?: number; title?: string; titleAs?: HeadingType; @@ -891,6 +960,7 @@ export const DataCard = React.forwardRef( asset, headline, pretitle, + pretitleAs, pretitleLinesMax, title, titleAs = 'h3', @@ -921,7 +991,12 @@ export const DataCard = React.forwardRef( const ariaLabel = ariaLabelProp || - [title, headlineText, pretitle, description, extraText].filter(Boolean).join(' '); + (isBiggerHeading(titleAs, pretitleAs) + ? [title, headlineText, pretitle, subtitle, description, extraText] + : [pretitle, headlineText, title, subtitle, description, extraText] + ) + .filter(Boolean) + .join(' '); return ( ( aria-label={isTouchable ? ariaLabel : undefined} > {isTouchable &&
} -
+
{asset && ( @@ -957,6 +1032,7 @@ export const DataCard = React.forwardRef( headline={headline} headlineRef={headlineRef} pretitle={pretitle} + pretitleAs={pretitleAs} pretitleLinesMax={pretitleLinesMax} title={title} titleAs={titleAs} @@ -1070,7 +1146,7 @@ export const SnapCard = React.forwardRef( aria-label={isTouchable ? ariaLabel : undefined} > {isTouchable &&
} -
+
{asset && (
( interface DisplayCardContentProps { title?: React.ReactNode; + titleAs?: HeadingType; headline?: React.ReactNode; pretitle?: React.ReactNode; + pretitleAs?: HeadingType; subtitle?: React.ReactNode; description?: React.ReactNode; extra?: React.ReactNode; @@ -1147,8 +1225,10 @@ interface DisplayCardContentProps { const DisplayCardContent = ({ title, + titleAs = 'h3', headline, pretitle, + pretitleAs, subtitle, description, extra, @@ -1158,21 +1238,44 @@ const DisplayCardContent = ({ // 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} -
+ {isBiggerHeading(titleAs, pretitleAs) ? ( + <> + {title && ( +
+ {title} +
+ )} + {headline && ( + // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description +
+ {headline} +
+ )} + {pretitle && ( +
+ {pretitle} +
+ )} + + ) : ( + <> + {pretitle && ( +
+ {pretitle} +
+ )} + {headline && ( + // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description +
+ {headline} +
+ )} + {title && ( +
+ {title} +
+ )} + )} {subtitle && ( @@ -1212,6 +1315,7 @@ interface CommonDisplayCardProps { dataAttributes?: DataAttributes; headline?: React.ReactComponentElement; pretitle?: string; + pretitleAs?: HeadingType; pretitleLinesMax?: number; title: string; titleAs?: HeadingType; @@ -1275,6 +1379,7 @@ const DisplayCard = React.forwardRef( asset, headline, pretitle, + pretitleAs, pretitleLinesMax, title, titleAs = 'h3', @@ -1323,7 +1428,12 @@ const DisplayCard = React.forwardRef( const ariaLabel = ariaLabelProp || - [title, headlineText, pretitle, description, extraText].filter(Boolean).join(' '); + (isBiggerHeading(titleAs, pretitleAs) + ? [title, headlineText, pretitle, description, extraText] + : [pretitle, headlineText, title, description, extraText] + ) + .filter(Boolean) + .join(' '); return ( ( > {isTouchable &&
} -
+
{(hasImage || hasVideo) && (
@@ -1411,13 +1521,14 @@ const DisplayCard = React.forwardRef( ) : undefined } + titleAs={titleAs} headline={headline} pretitle={ pretitle ? ( @@ -1425,6 +1536,7 @@ const DisplayCard = React.forwardRef( ) : undefined } + pretitleAs={pretitleAs} description={ description ? ( ; pretitle?: string; + pretitleAs?: HeadingType; pretitleLinesMax?: number; title?: string; titleAs?: HeadingType; @@ -1568,6 +1681,7 @@ export const PosterCard = React.forwardRef( asset, headline, pretitle, + pretitleAs, pretitleLinesMax, title, titleAs = 'h3', @@ -1633,7 +1747,12 @@ export const PosterCard = React.forwardRef( const ariaLabel = ariaLabelProp || - [title, headlineText, pretitle, subtitle, description, extraText].filter(Boolean).join(' '); + (isBiggerHeading(titleAs, pretitleAs) + ? [title, headlineText, pretitle, subtitle, description, extraText] + : [pretitle, headlineText, title, subtitle, description, extraText] + ) + .filter(Boolean) + .join(' '); return ( ( ) : undefined } + titleAs={titleAs} headline={headline} pretitle={ pretitle ? ( @@ -1738,6 +1858,7 @@ export const PosterCard = React.forwardRef( ) : undefined } + pretitleAs={pretitleAs} subtitle={ subtitle ? ( = ({ headlineType, headline, pretitle, + pretitleAs, title, + titleAs, subtitle, description, stackingGroup, @@ -86,7 +91,9 @@ export const Default: StoryComponent = ({ } headline={headline ? {headline} : undefined} pretitle={pretitle} + pretitleAs={pretitleAs} title={title} + titleAs={titleAs} subtitle={subtitle} description={description} aria-label="aria label" @@ -109,7 +116,9 @@ Default.args = { headlineType: 'promo', headline: 'headline', pretitle: 'pretitle', + pretitleAs: 'span', title: 'title', + titleAs: 'h3', subtitle: 'subtitle', description: 'description', stackingGroup: true, @@ -138,4 +147,12 @@ Default.argTypes = { ], control: {type: 'select'}, }, + pretitleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, + titleAs: { + options: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span'], + control: {type: 'select'}, + }, }; diff --git a/src/community/advanced-data-card.css.ts b/src/community/advanced-data-card.css.ts index ff39516781..0fbea9ffd9 100644 --- a/src/community/advanced-data-card.css.ts +++ b/src/community/advanced-data-card.css.ts @@ -264,3 +264,8 @@ export const topActionsWithoutIcon = style({ }, }, }); + +export const flexColumn = style({ + display: 'flex', + flexDirection: 'column', +}); diff --git a/src/community/advanced-data-card.tsx b/src/community/advanced-data-card.tsx index 1f945d056b..4c35f89b26 100644 --- a/src/community/advanced-data-card.tsx +++ b/src/community/advanced-data-card.tsx @@ -13,11 +13,13 @@ import {vars} from '../skins/skin-contract.css'; import Box from '../box'; import Touchable from '../touchable'; import classNames from 'classnames'; -import {CardActionsGroup} from '../card'; +import {CardActionsGroup, useInnerText} from '../card'; import {useTheme} from '../hooks'; import {getPrefixedDataAttributes} from '../utils/dom'; import Inline from '../inline'; import {applyCssVars} from '../utils/css'; +import Tag from '../tag'; +import {isBiggerHeading} from '../utils/headings'; import type {PressHandler} from '../touchable'; import type {ExclusifyUnion} from '../utils/utility-types'; @@ -27,7 +29,6 @@ import type Image from '../image'; import type {ButtonPrimary, ButtonLink} from '../button'; import type {DataAttributes, HeadingType, TrackingEvent} from '../utils/types'; import type {RendersNullableElement} from '../utils/renders-element'; -import type Tag from '../tag'; import type { HighlightedValueBlock, InformationBlock, @@ -59,11 +60,12 @@ type MaybeTouchableCard = ExclusifyUnion | T>; type CardContentProps = { headline?: string | RendersNullableElement; + headlineRef?: (instance: HTMLElement | null) => void; pretitle?: string; - pretitleAs?: string; + pretitleAs?: HeadingType; pretitleLinesMax?: number; title?: string; - titleAs?: string; + titleAs: HeadingType; titleLinesMax?: number; subtitle?: string; subtitleLinesMax?: number; @@ -73,11 +75,12 @@ type CardContentProps = { const CardContent = ({ headline, + headlineRef, pretitle, - pretitleAs = 'p', + pretitleAs, pretitleLinesMax, title, - titleAs = 'h3', + titleAs, titleLinesMax, subtitle, subtitleLinesMax, @@ -87,36 +90,78 @@ const CardContent = ({ const {textPresets} = useTheme(); return ( - - {headline} - {pretitle && ( - - {pretitle} - + /** using flex instead of nested Stacks, this way we can rearrange texts so the DOM structure makes more sense for screen reader users */ +
+ {isBiggerHeading(titleAs, pretitleAs) ? ( + <> + {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} + +
+ )} + + ) : ( + <> + {pretitle && ( +
+ + {pretitle} + +
+ )} + {headline && ( + // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description +
+ {typeof headline === 'string' ? {headline} : headline} +
+ )} + {title && ( +
+ + {title} + +
+ )} + + )} + {subtitle && ( +
+ + {subtitle} + +
)} - - - {title} - - - {subtitle} - {description && ( )} - +
); }; @@ -246,7 +291,7 @@ export const AdvancedDataCard = React.forwardRef @@ -308,6 +366,7 @@ export const AdvancedDataCard = React.forwardRef
{hasExtras && ( -
+
{extra.map((item, index) => { return (
diff --git a/src/cover-hero.css.ts b/src/cover-hero.css.ts index 921f992dcc..224a816150 100644 --- a/src/cover-hero.css.ts +++ b/src/cover-hero.css.ts @@ -126,3 +126,8 @@ export const sixColumns = style({ }); export const sideExtra = sprinkles({position: 'relative'}); + +export const flexColumn = sprinkles({ + display: 'flex', + flexDirection: 'column', +}); diff --git a/src/cover-hero.tsx b/src/cover-hero.tsx index 08520b28ed..d2096ca0f7 100644 --- a/src/cover-hero.tsx +++ b/src/cover-hero.tsx @@ -12,6 +12,7 @@ import * as mediaStyles from './image.css'; import GridLayout from './grid-layout'; import {CoverHeroMedia} from './cover-hero-media'; import {getPrefixedDataAttributes} from './utils/dom'; +import {isBiggerHeading} from './utils/headings'; import type {DataAttributes, HeadingType} from './utils/types'; import type {ImageProps, VideoProps} from './cover-hero-media'; @@ -26,6 +27,7 @@ type BaseProps = { headline?: RendersNullableElement; pretitle?: string; pretitleLinesMax?: number; + pretitleAs?: HeadingType; title: string; titleLinesMax?: number; titleAs?: HeadingType; @@ -73,6 +75,7 @@ const CoverHero = React.forwardRef( headline, pretitle, pretitleLinesMax, + pretitleAs, title, titleLinesMax, titleAs = 'h1', @@ -108,39 +111,70 @@ const CoverHero = React.forwardRef( const textShadow = hasMedia ? '0 0 15px rgba(0, 0, 0, 0.4)' : undefined; + const pretitleContent = pretitle ? ( + + {pretitle} + + ) : undefined; + + const titleContent = title ? ( + + {title} + + ) : undefined; + + const headlineContent = headline ? ( +
+ + {headline} + +
+ ) : undefined; + const mainContent = (
- {headline && ( - - {headline} - - )} - - {pretitle && ( -
- - {pretitle} - -
+ {/** using flex instead of nested Stacks, this way we can rearrange texts so the DOM structure makes more sense for screen reader users */} +
+ {isBiggerHeading(titleAs, pretitleAs) ? ( + <> + {titleContent} + {headlineContent} + {pretitleContent && ( +
+ {pretitleContent} +
+ )} + + ) : ( + <> + {pretitleContent && ( +
+ {pretitleContent} +
+ )} + {headlineContent} + {titleContent} + )} - - {title} - - +
{description && (
+ {title} + + ) : ( + + {title} + + ) + ) : undefined; + + const headlineContent = headline ? ( + // assuming that the headline will always be followed by one of: pretitle, title, description +
+ {headline} +
+ ) : undefined; + return ( {(title || pretitle || description) && ( - - {headline &&
{headline}
} - {renderPretitle()} - {title && - (small ? ( - - {title} - - ) : ( - - {title} - - ))} - {description && - (small ? ( - - {description} - - ) : ( - - {description} - - ))} -
+ {/** using flex instead of nested Stacks, this way we can rearrange texts so the DOM structure makes more sense for screen reader users */} +
+ {isBiggerHeading(titleAs, pretitleAs) ? ( + <> + {titleContent && ( +
{titleContent}
+ )} + {headlineContent} + {pretitleContent && ( +
+ {pretitleContent} +
+ )} + + ) : ( + <> + {pretitleContent && ( +
+ {pretitleContent} +
+ )} + {headlineContent} + {titleContent && ( +
{titleContent}
+ )} + + )} + + {description && ( +
+ {small ? ( + + {description} + + ) : ( + + {description} + + )} +
+ )} +
)}
@@ -184,7 +229,7 @@ export const HeaderLayout = ({ return (
- + { type HeroContentProps = { headline?: RendersNullableElement; pretitle?: string; + pretitleAs?: HeadingType; title?: string; titleAs?: HeadingType; description?: string; @@ -64,6 +71,7 @@ const HeroContent = ({ title, titleAs = 'h1', pretitle, + pretitleAs, description, descriptionLinesMax, extra, @@ -71,23 +79,52 @@ const HeroContent = ({ secondaryButton, buttonLink, }: HeroContentProps) => { + const titleContent = title ? ( + + {title} + + ) : undefined; + + const pretitleContent = pretitle ? ( + + {pretitle} + + ) : undefined; + + const headlineContent = headline ? ( + // assuming that the headline will always be followed by one of: pretitle, title, subtitle, description +
+ {headline} +
+ ) : undefined; + return (
- {headline &&
{headline}
} - - {pretitle && ( - - {pretitle} - - )} - {title && ( - - {title} - + {/** using flex instead of nested Stacks, this way we can rearrange texts so the DOM structure makes more sense for screen reader users */} +
+ {isBiggerHeading(titleAs, pretitleAs) ? ( + <> + {titleContent} + {headlineContent} + {pretitleContent && ( +
+ {pretitleContent} +
+ )} + + ) : ( + <> + {pretitleContent && ( +
{pretitleContent}
+ )} + {headlineContent} + {titleContent} + )} - +
+ {description && ( | RendersElement; headline?: RendersNullableElement; pretitle?: string; + pretitleAs?: HeadingType; title?: string; titleAs?: HeadingType; description?: string; diff --git a/src/utils/headings.tsx b/src/utils/headings.tsx new file mode 100644 index 0000000000..9fb25bf1aa --- /dev/null +++ b/src/utils/headings.tsx @@ -0,0 +1,15 @@ +import type {HeadingType} from './types'; + +export const isBiggerHeading = (heading: HeadingType, otherHeading?: HeadingType): boolean => { + // In case headings are equal, we consider that the first one has more priority + if (!otherHeading || heading === otherHeading) { + return true; + } + + if (heading === 'span') { + return false; + } + + // Both are header tags + return heading < otherHeading; +};