Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

[WNMGDS-3191] Box Content Component #3419

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
abffb46
Initial styling and component setup
jack-ryan-nava-pbc Jan 29, 2025
1ee0b46
Adding support for quotations
jack-ryan-nava-pbc Jan 29, 2025
d7694e9
Refining styling
jack-ryan-nava-pbc Jan 30, 2025
f4264e2
[WNMGDS-3183] Validate compatibility with different React versions th…
pwolfert Jan 29, 2025
d43477b
[WNMGDS-3166] Add eMedicare footer web-component (#3405)
tamara-corbalt Jan 31, 2025
b29fbf3
Author and citation logic implemented
jack-ryan-nava-pbc Jan 31, 2025
778c011
Beginnging tests
jack-ryan-nava-pbc Jan 31, 2025
0fe52af
Merge branch 'main' into jryan/WNMGDS-3191-box-content
jack-ryan-nava-pbc Jan 31, 2025
a26c38a
Updated tests, use icon and update display logic a bit
jack-ryan-nava-pbc Feb 3, 2025
0dfcf0f
Export the BoxContent component
jack-ryan-nava-pbc Feb 3, 2025
9a7d854
New VRTs
jack-ryan-nava-pbc Feb 3, 2025
92cd24e
Update props table
jack-ryan-nava-pbc Feb 3, 2025
6d8f332
Update tests clean up logic in BoxQuotation
jack-ryan-nava-pbc Feb 4, 2025
2576e3d
Simple clean up
jack-ryan-nava-pbc Feb 4, 2025
a448676
Make BoxContent more generic
jack-ryan-nava-pbc Feb 4, 2025
4b3c544
Update specs
jack-ryan-nava-pbc Feb 4, 2025
dfb1485
Merge branch 'main' into jryan/WNMGDS-3191-box-content
jack-ryan-nava-pbc Feb 4, 2025
70b27b7
snapshots after refactor
jack-ryan-nava-pbc Feb 4, 2025
93355f1
Support forced colors mode and simplify some vars
jack-ryan-nava-pbc Feb 5, 2025
f00e06a
Allow for additional classes to be added to the box quotation element.
jack-ryan-nava-pbc Feb 5, 2025
d244b22
dont wrap icon in h2 and update specs
jack-ryan-nava-pbc Feb 5, 2025
ced1952
Update snapshots again
jack-ryan-nava-pbc Feb 5, 2025
485d679
Merge branch 'main' into jryan/WNMGDS-3191-box-content
jack-ryan-nava-pbc Feb 6, 2025
20b2062
Update citation styling
jack-ryan-nava-pbc Feb 6, 2025
3597da8
Attempt at nested stories
jack-ryan-nava-pbc Feb 7, 2025
9413587
Separated boxquotation into a separate story
jack-ryan-nava-pbc Feb 7, 2025
2e61ba9
Add underlying html to storybook props update tests and snapshots and…
jack-ryan-nava-pbc Feb 10, 2025
8311618
Merge branch 'main' into jryan/WNMGDS-3191-box-content
jack-ryan-nava-pbc Feb 10, 2025
46befda
Update snapshots
jack-ryan-nava-pbc Feb 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import type { Meta, StoryObj } from '@storybook/react';
import BoxContent from './BoxContent';

const meta: Meta = {
component: BoxContent,
argTypes: {
children: { control: 'text' },
bordered: { control: 'boolean', defaultValue: true },
heading: { control: 'text' },
headingLevel: { control: 'text' },
},
parameters: {
docs: {
underlyingHtmlElements: ['aside'],
},
},
};

export default meta;
type Story = StoryObj<typeof BoxContent>;

const BoxContentTemplate: Story = {
render: ({ ...args }) => {
return <BoxContent {...args}>{args.children}</BoxContent>;
},
};

export const Default = {
...BoxContentTemplate,
args: {
heading: 'The Inflation Reduction Act',
children:
"The Inflation Reduction Act keeps these savings and lower costs through 2025. If you qualify for savings, you'll find out the lower costs when you shop for plans.",
bordered: true,
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { QuotationMarkIcon } from '../Icons';
import BoxContent from './BoxContent';
import { BoxQuotation } from './BoxQuotation';
import { render, screen } from '@testing-library/react';

const defaultProps = {
children: 'This is foo text. Bar!',
heading: 'Test Heading',
};

function renderBoxContent(props = {}) {
return render(<BoxContent {...defaultProps} {...props} />);
}

describe('BoxContent', () => {
it('renders as complementary content', () => {
renderBoxContent();
expect(screen.getByRole('complementary')).toMatchSnapshot();
});

it('renders heading properly', () => {
renderBoxContent();
const headingElement = screen.getByText(/Test Heading/);
expect(headingElement).toBeInTheDocument();
});

it('renders just child content', () => {
renderBoxContent({ heading: null });
const headingElement = screen.queryByText(/Test Heading/);
const childElement = screen.getByText(/This is foo text. Bar!/);
expect(childElement).toBeInTheDocument();
expect(headingElement).not.toBeInTheDocument();
});

it('renders border properly', () => {
const { container } = renderBoxContent({ bordered: true });
expect(container.firstChild).toHaveClass('ds-c-box-content--bordered');
});

it('renders quote option properly', () => {
renderBoxContent({
heading: <QuotationMarkIcon />,
children: (
<BoxQuotation author="Test Author" citation="Test Citation">
This is foo text. Bar!
</BoxQuotation>
),
});
const quoteElement = screen.getByText(/This is foo text. Bar!/);
expect(quoteElement).toBeInTheDocument();
// Per Design request citation wins out over author if both are provided
const authorElement = screen.queryByText(/Test Author/);
expect(authorElement).not.toBeInTheDocument();
const citationElement = screen.getByText(/Test Citation/);
expect(citationElement).toBeInTheDocument();
expect(screen.getByRole('complementary')).toMatchSnapshot();
});

it('renders a quote with only author', () => {
renderBoxContent({
heading: <QuotationMarkIcon />,
children: <BoxQuotation author="Test Author">This is foo text. Bar!</BoxQuotation>,
});
const authorElement = screen.getByText(/Test Author/);
expect(authorElement).toBeInTheDocument();
});

it('renders a quote with only citation', () => {
renderBoxContent({
heading: <QuotationMarkIcon />,
children: <BoxQuotation citation="Test Citation">This is foo text. Bar!</BoxQuotation>,
});
const citationElement = screen.getByText(/Test Citation/);
expect(citationElement).toBeInTheDocument();
});

it('renders children properly', () => {
renderBoxContent();
const childrenElement = screen.getByText(/This is foo text. Bar!/);
expect(childrenElement).toBeInTheDocument();
});

it('applies className properly', () => {
const { container } = renderBoxContent({ className: 'test-class' });
expect(container.firstChild).toHaveClass('test-class');
});

it('applies heading level properly', () => {
renderBoxContent({ heading: 'Test Heading', headingLevel: '3' });
const headingElement = screen.getByRole('heading', { level: 3 });
expect(headingElement).toBeInTheDocument();
});
});
56 changes: 56 additions & 0 deletions packages/design-system/src/components/BoxContent/BoxContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import type * as React from 'react';
import { FunctionComponent } from 'react';
import classNames from 'classnames';

export type BoxContentHeadingLevel = '2' | '3' | '4' | '5' | '6';

interface BoxContentProps {
/**
* Applies a border to the box content.
*/
bordered?: boolean;
/**
* Content to be displayed within the Box
*/
children: React.ReactNode;
/**
* Additional classes to be added to the component
*/
className?: string;
/**
* Text for the box content heading
*/
heading?: React.ReactNode;
/**
* Heading type to override default `<h2>`.
*/
headingLevel?: BoxContentHeadingLevel;
}

const BoxContent: FunctionComponent<BoxContentProps> = (props: BoxContentProps) => {
const { bordered, children, className, heading, headingLevel = '2' } = props;

const classes = classNames(
'ds-c-box-content',
bordered && 'ds-c-box-content--bordered',
className
);
let headingElement;
if (typeof heading === 'string') {
const Heading = `h${headingLevel}` as const;
headingElement = <Heading className="ds-c-box-content__heading">{heading}</Heading>;
} else {
headingElement = heading;
}

return (
<aside className={classes}>
<div className="ds-c-box-content__body">
{headingElement}
<div className={heading ? 'ds-c-box-content__text' : ''}>{children}</div>
</div>
</aside>
);
};

export default BoxContent;
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { Meta, StoryObj } from '@storybook/react';
import BoxContent from './BoxContent';
import { BoxQuotation } from './BoxQuotation';
import { QuotationMarkIcon } from '../Icons';

const boxContentMeta: Meta = {
title: 'Components/BoxQuotation',
component: BoxQuotation,
argTypes: {
author: { control: 'text' },
citation: { control: 'text' },
children: { control: 'text' },
},
parameters: {
docs: {
underlyingHtmlElements: ['blockquote', 'figure', 'figcaption', 'cite'],
},
},
};
export default boxContentMeta;
type Story = StoryObj<typeof BoxQuotation>;

const BoxQuotationStory: Story = {
render: ({ ...args }) => {
return (
<BoxContent heading={<QuotationMarkIcon />}>
<BoxQuotation {...args}>{args.children}</BoxQuotation>
</BoxContent>
);
},
};

export const Default = {
...BoxQuotationStory,
args: {
citation: <a href="https://home.treasury.gov/">U.S. Department of the Treasury</a>,
children:
"The U.S. Department of the Treasury's mission is to maintain a strong economy and create economic and job opportunities by promoting the conditions that enable economic growth and stability at home and abroad, strengthen national security by combating threats and protecting the integrity of the financial system, and manage the U.S. Government’s finances and resources effectively.",
},
};

export const WithAuthor = {
...BoxQuotationStory,
args: {
author: 'John Adams',
children: 'Let us dare to read, think, speak and write.',
},
};
54 changes: 54 additions & 0 deletions packages/design-system/src/components/BoxContent/BoxQuotation.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type * as React from 'react';
import { FunctionComponent } from 'react';
import { RequireAtLeastOne } from '../utilities/requireAtLeastOne';
import classNames from 'classnames';

interface MinimumBoxQuotationProps {
/**
* Provide an author for the quote
*/
author?: string;
/**
* Content to be displayed within the Box Quotation
*/
children: React.ReactNode;
/**
* Provide a citation for the quote
*/
citation?: React.ReactNode;
/**
* Additional classes to be added to the component
*/
className?: string;
}

export type BoxQuotationProps = RequireAtLeastOne<MinimumBoxQuotationProps, 'citation' | 'author'>;
jack-ryan-nava-pbc marked this conversation as resolved.
Show resolved Hide resolved

export const BoxQuotation: FunctionComponent<BoxQuotationProps> = (props: BoxQuotationProps) => {
const { author, children, citation, className } = props;

const classes = classNames('ds-c-box-content-quotation', className);

const captionContent = () => {
// We want to prioritize citations over authors, so if both are present only render the citation.
if (citation) {
return <cite className="ds-c-box-content-quotation--citation">{citation}</cite>;
}
// If citation is not present, but author is, render the author.
if (author) {
return `${author} `;
}
if (!citation && !author) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a bit heavy handed???

throw new Error('Either a citation or an author is required for using a BoxQuotation.');
}
};

return (
<figure className={classes}>
<blockquote className="ds-c-box-content-quotation--blockquote">{children}</blockquote>
<figcaption className="ds-c-box-content-quotation--caption">
{`\u2014`} {captionContent()}
</figcaption>
tamara-corbalt marked this conversation as resolved.
Show resolved Hide resolved
</figure>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`BoxContent renders as complementary content 1`] = `
<aside
class="ds-c-box-content"
>
<div
class="ds-c-box-content__body"
>
<h2
class="ds-c-box-content__heading"
>
Test Heading
</h2>
<div
class="ds-c-box-content__text"
>
This is foo text. Bar!
</div>
</div>
</aside>
`;

exports[`BoxContent renders quote option properly 1`] = `
<aside
class="ds-c-box-content"
>
<div
class="ds-c-box-content__body"
>
<svg
aria-hidden="true"
class="ds-c-icon ds-c-icon--left-quote "
viewBox="2 0 40 32"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 12.5C3 6.69875 7.67478 2 13.4464 2H14.1429C15.6837 2 16.9286 3.25125 16.9286 4.8C16.9286 6.34875 15.6837 7.6 14.1429 7.6H13.4464C10.7565 7.6 8.57143 9.79625 8.57143 12.5V13.2H14.1429C17.2158 13.2 19.7143 15.7113 19.7143 18.8V24.4C19.7143 27.4887 17.2158 30 14.1429 30H8.57143C5.49844 30 3 27.4887 3 24.4V12.5ZM25.2857 12.5C25.2857 6.69875 29.9605 2 35.7321 2H36.4286C37.9694 2 39.2143 3.25125 39.2143 4.8C39.2143 6.34875 37.9694 7.6 36.4286 7.6H35.7321C33.0422 7.6 30.8571 9.79625 30.8571 12.5V13.2H36.4286C39.5016 13.2 42 15.7113 42 18.8V24.4C42 27.4887 39.5016 30 36.4286 30H30.8571C27.7842 30 25.2857 27.4887 25.2857 24.4V12.5Z"
/>
</svg>
<div
class="ds-c-box-content__text"
>
<figure
class="ds-c-box-content-quotation"
>
<blockquote
class="ds-c-box-content-quotation--blockquote"
>
This is foo text. Bar!
</blockquote>
<figcaption
class="ds-c-box-content-quotation--caption"
>

<cite
class="ds-c-box-content-quotation--citation"
>
Test Citation
</cite>
</figcaption>
</figure>
</div>
</div>
</aside>
`;
1 change: 1 addition & 0 deletions packages/design-system/src/components/BoxContent/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './BoxContent';
1 change: 1 addition & 0 deletions packages/design-system/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './Accordion';
export * from './Alert';
export * from './Autocomplete';
export * from './Badge';
export * from './BoxContent';
export * from './Button';
export * from './ChoiceList';
export * from './DateField';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type RequireAtLeastOne<T, Keys extends keyof T = keyof T> = Pick<T, Exclude<keyof T, Keys>> &
{
[K in Keys]-?: Required<Pick<T, K>> & Partial<Pick<T, Exclude<Keys, K>>>;
}[Keys];
Loading