diff --git a/.storybook/main.js b/.storybook/main.js index 59c4bb9b..5470a9ac 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -6,8 +6,7 @@ module.exports = { '@storybook/addon-essentials', '@storybook/addon-interactions', '@storybook/addon-a11y', - 'display-element-css', - 'storybook-dark-mode' + 'display-element-css' ], docs: { autodocs: true, diff --git a/.storybook/manager-head.html b/.storybook/manager-head.html index 16b755e1..55577966 100644 --- a/.storybook/manager-head.html +++ b/.storybook/manager-head.html @@ -3,9 +3,22 @@ font-size: 15px !important; } - div.sidebar-item[data-selected="false"]:hover, + /* Sidebar hover */ + div.sidebar-item[data-selected='false']:hover, button.sidebar-item:hover { background-color: #5a5d61 !important; color: white !important; } - \ No newline at end of file + + /* Sidebar hover/selected icons */ + div.sidebar-item[data-selected='false']:hover svg, + button.sidebar-item:hover svg { + color: white; + } + + /* Sidebar icons */ + .sidebar-item svg { + color: black; + } + + diff --git a/.storybook/manager.js b/.storybook/manager.js index d6d1c2a9..6fc4ea24 100644 --- a/.storybook/manager.js +++ b/.storybook/manager.js @@ -1,4 +1,5 @@ import { addons } from '@storybook/manager-api'; +import themeCFPB from './themeCFPB'; addons.setConfig({ sidebar: { @@ -11,5 +12,6 @@ addons.setConfig({ } return name; } - } + }, + theme: themeCFPB }); diff --git a/.storybook/preview.js b/.storybook/preview.js index 63a108de..de5399e9 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,42 +1,26 @@ import '@cfpb/cfpb-design-system/src/cfpb-design-system.less'; -import { themes } from '@storybook/theming'; -import CfpbLogo from '../src/assets/images/cfpb-logo-vertical.png'; import '../src/assets/styles/_shared.less'; +import themeCFPB from './themeCFPB'; -const sharedThemeElements = { - brandTitle: 'CFPB Design System React', - brandImage: CfpbLogo, - brandUrl: 'https://cfpb.github.io/design-system/', - brandTarget: '_blank', - fontBase: '"Avenir Next", Arial ,sans-serif' -}; - -export const parameters = { - options: { - // Determines the display order of Stories in the sidebar - storySort: { - order: ['Guides', 'Components (Verified)', 'Components (Draft)', '*'] - } - }, - actions: { argTypesRegex: '^on[A-Z].*' }, - controls: { - matchers: { - color: /(background|color)$/i, - date: /Date$/ - } - }, - darkMode: { - current: 'dark', - // Override the default dark theme - dark: { - ...themes.dark, - ...sharedThemeElements, - appBg: 'black' +export const preview = { + parameters: { + options: { + // Determines the display order of Stories in the sidebar + storySort: { + order: ['Guides', 'Components (Verified)', 'Components (Draft)', '*'] + } + }, + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/ + } }, - // Override the default light theme - light: { - ...themes.normal, - ...sharedThemeElements + docs: { + theme: themeCFPB } } }; + +export default preview; diff --git a/.storybook/themeCFPB.js b/.storybook/themeCFPB.js new file mode 100644 index 00000000..8f3e9e21 --- /dev/null +++ b/.storybook/themeCFPB.js @@ -0,0 +1,53 @@ +import { create } from '@storybook/theming/create'; +import CfpbLogo from '../src/assets/images/cfpb-logo-vertical.png'; + +const colors = { + black: '#101820', + gray: '#5a5d61', + gray10: '#e7e8e9', + gray40: '#b4b5b6', + green10: '#f0f8eb', + teal: '#257675', + teal80: '#579695' +}; + +/** + * Note: Additional CSS customizations are implemented in + * - /.storybook/manager-head.html + * - /src/assets/styles/_shared.less + */ +export default create({ + base: 'light', + + brandTitle: 'CFPB Design System React', + brandImage: CfpbLogo, + brandUrl: 'https://cfpb.github.io/design-system/', + brandTarget: '_blank', + + fontBase: '"Avenir Next", Arial ,sans-serif', + + // App + appBorderColor: colors.gray, + appContentBg: 'white', // Story overview, tool panel + + // Sidebar + appBg: 'white', // Background + textColor: colors.black, // Story names + textMutedColor: colors.black, // Group names + + // Toolbars + // colorPrimary: 'color', // Purpose unknown + colorSecondary: colors.teal, // Selected (Controls, Actions, etc) + barTextColor: colors.black, // Text & icons + barSelectedColor: colors.teal, + barBg: colors.gray10, + + // Form colors (Controls) + buttonBg: colors.gray10, + buttonBorder: colors.gray40, + booleanBg: colors.gray10, + booleanSelectedBg: colors.teal80, + inputBg: 'white', + inputBorder: colors.gray, + inputTextColor: colors.black +}); diff --git a/package.json b/package.json index 1d09fc30..dba6ea34 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,6 @@ "prettier": "2.7.1", "start-server-and-test": "1.14.0", "storybook": "^7.0.6", - "storybook-dark-mode": "^3.0.1", "stylelint": "14.15.0", "stylelint-config-prettier": "9.0.4", "stylelint-config-standard": "29.0.0", diff --git a/src/assets/images/cfpb-logo-vertical.png b/src/assets/images/cfpb-logo-vertical.png index 10b62026..3cc78b68 100644 Binary files a/src/assets/images/cfpb-logo-vertical.png and b/src/assets/images/cfpb-logo-vertical.png differ diff --git a/src/assets/styles/_shared.less b/src/assets/styles/_shared.less index a96f575c..62169aa4 100644 --- a/src/assets/styles/_shared.less +++ b/src/assets/styles/_shared.less @@ -57,3 +57,10 @@ pre.prismjs, :where(p:not(.sb-story *, #storybook-root *)) { line-height: 22px !important; } + +/* +Apply a global line-height that doesn't interfere with component Stories that have their own line-height settings. +*/ +:where(p:not(.sb-story *, #storybook-root *)){ + line-height: 22px !important; +} diff --git a/src/components/Paragraph/Paragraph.test.tsx b/src/components/Paragraph/Paragraph.test.tsx new file mode 100644 index 00000000..b9a0211e --- /dev/null +++ b/src/components/Paragraph/Paragraph.test.tsx @@ -0,0 +1,19 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { Paragraph } from './Paragraph'; + +describe('', () => { + const helperText = 'helperText goes here'; + + it('renders Body text', () => { + render({helperText}); + expect(screen.getByText(helperText)).toBeInTheDocument(); + }); + + it('renders Lead paragraph', () => { + render({helperText}); + const element = screen.getByText(helperText); + expect(element).toBeInTheDocument(); + expect(element.classList.contains('lead-paragraph')).toBe(true); + }); +}); diff --git a/src/components/Paragraph/Paragraph.tsx b/src/components/Paragraph/Paragraph.tsx new file mode 100644 index 00000000..806052ce --- /dev/null +++ b/src/components/Paragraph/Paragraph.tsx @@ -0,0 +1,29 @@ +import classnames from 'classnames'; + +interface ParagraphProperties extends React.HTMLProps { + isLead?: boolean; +} + +/** + * Paragraph text should provide an efficient and pleasant experience on every viewport size. Readable text makes good use of alignment, spacing, line length and height, and contrast. + * + * Source: https://cfpb.github.io/design-system/foundation/paragraphs + */ +export function Paragraph({ + children, + isLead, + className, + ...properties +}: ParagraphProperties): JSX.Element { + const cnames = [className]; + + if (isLead) cnames.push('lead-paragraph'); + + return ( +

+ {children} +

+ ); +} + +export default Paragraph; diff --git a/src/components/Paragraph/Paragraphs.stories.tsx b/src/components/Paragraph/Paragraphs.stories.tsx new file mode 100644 index 00000000..2a3fddb1 --- /dev/null +++ b/src/components/Paragraph/Paragraphs.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta } from '@storybook/react'; +import { Paragraph } from './Paragraph'; + +const meta: Meta = { + title: 'Components (Verified)/Paragraphs', + component: Paragraph +}; + +export default meta; + +export const Body = { + name: 'Body text', + args: { + children: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore.' + } +}; + +export const Lead = { + name: 'Lead paragraph', + args: { + children: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.', + isLead: true + } +}; diff --git a/src/components/TextIntroduction/TextIntroduction.stories.tsx b/src/components/TextIntroduction/TextIntroduction.stories.tsx new file mode 100644 index 00000000..af5b0fdc --- /dev/null +++ b/src/components/TextIntroduction/TextIntroduction.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta } from '@storybook/react'; +import { TextIntroduction } from './TextIntroduction'; +import placeholders from './testHelpers'; + +const meta: Meta = { + title: 'Components (Verified)/Text introductions', + component: TextIntroduction +}; + +export default meta; + +export const Standard = { + name: 'Standard text introduction', + args: { + ...placeholders + } +}; diff --git a/src/components/TextIntroduction/TextIntroduction.test.tsx b/src/components/TextIntroduction/TextIntroduction.test.tsx new file mode 100644 index 00000000..9935743b --- /dev/null +++ b/src/components/TextIntroduction/TextIntroduction.test.tsx @@ -0,0 +1,14 @@ +import '@testing-library/jest-dom'; +import { render, screen } from '@testing-library/react'; +import { TextIntroduction } from './TextIntroduction'; +import placeholders from './testHelpers'; + +describe('', () => { + it('renders all elements when provided', () => { + render(); + expect(screen.getByText(placeholders.heading)).toBeInTheDocument(); + expect(screen.getByText(placeholders.subheading)).toBeInTheDocument(); + expect(screen.getByText(placeholders.description)).toBeInTheDocument(); + expect(screen.getByText('Call-to-action link')).toBeInTheDocument(); + }); +}); diff --git a/src/components/TextIntroduction/TextIntroduction.tsx b/src/components/TextIntroduction/TextIntroduction.tsx new file mode 100644 index 00000000..51e77b1b --- /dev/null +++ b/src/components/TextIntroduction/TextIntroduction.tsx @@ -0,0 +1,54 @@ +import classnames from 'classnames'; +import { cloneElement, type ReactNode } from 'react'; +import { Heading } from '../Headings/Heading'; +import List from '../List/List'; +import ListItem from '../List/ListItem'; +import { Paragraph } from '../Paragraph/Paragraph'; + +interface TextIntroductionProperties extends React.HTMLProps { + // Page title + heading: string; + // Lead paragraph + subheading: string; + // Descriptive paragraph + description?: ReactNode; + // Call-to-action + callToAction?: JSX.Element; +} + +/** + * The text introduction is the standard page introduction pattern used across all pages that do not have a hero or item introduction. They introduce a page, or collection of pages, with a brief description of the goals of that section. + * + * Source: https://cfpb.github.io/design-system/patterns/text-introductions + */ +export const TextIntroduction = ({ + heading, + subheading, + description, + callToAction, + className, + ...properties +}: TextIntroductionProperties): JSX.Element => { + const cnames = ['o-text-introduction', className]; + + const call2action = callToAction && ( + + {cloneElement(callToAction, { type: 'list' })} + + ); + + return ( +
+ {heading} + {subheading} + {description ?

{description}

: null} + {call2action} +
+ ); +}; + +export default TextIntroduction; diff --git a/src/components/TextIntroduction/testHelpers.tsx b/src/components/TextIntroduction/testHelpers.tsx new file mode 100644 index 00000000..63a5c080 --- /dev/null +++ b/src/components/TextIntroduction/testHelpers.tsx @@ -0,0 +1,16 @@ +import Link from '../Link/Link'; + +const subheading = + 'Lead paragraph lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.'; + +const description = + 'Descriptive paragraph lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore.'; + +const callToAction = Call-to-action link; + +export default { + heading: 'Heading 1', + description, + subheading, + callToAction +}; diff --git a/src/index.ts b/src/index.ts index 8c4a0f24..585d6565 100644 --- a/src/index.ts +++ b/src/index.ts @@ -36,6 +36,7 @@ export { default as Navbar } from './components/Navbar/Navbar'; export { Notification } from './components/Notification/Notification'; export { default as PageHeader } from './components/PageHeader/PageHeader'; export { Pagination } from './components/Pagination/Pagination'; +export { Paragraph } from './components/Paragraph/Paragraph'; export { RadioButton } from './components/RadioButton/RadioButton'; export { TableComplex, @@ -44,5 +45,6 @@ export { } from './components/Table/Table'; export { Tagline } from './components/Tagline/Tagline'; export { TextInput } from './components/TextInput/TextInput'; +export { TextIntroduction } from './components/TextIntroduction/TextIntroduction'; export { default as Well } from './components/Well/Well'; diff --git a/yarn.lock b/yarn.lock index 2a284092..ce6f2922 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3721,38 +3721,6 @@ __metadata: languageName: node linkType: hard -"@storybook/addons@npm:^7.0.0": - version: 7.4.3 - resolution: "@storybook/addons@npm:7.4.3" - dependencies: - "@storybook/manager-api": 7.4.3 - "@storybook/preview-api": 7.4.3 - "@storybook/types": 7.4.3 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - checksum: 6572fe0f389e6c9ec97e0f49a7212b0c9dd8196d794e0557d743397b6a69bc472638b879b63b61dc03d5dda78052d74d07460df62afea7cecbfb9d39eef35171 - languageName: node - linkType: hard - -"@storybook/api@npm:^7.0.0": - version: 7.4.3 - resolution: "@storybook/api@npm:7.4.3" - dependencies: - "@storybook/client-logger": 7.4.3 - "@storybook/manager-api": 7.4.3 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: bf1afa0ae75e6e959e2d19c0fac27e63ab2ba83e79cd4eac461c8eb216127b02741ad7ae75dae7353fed0192b3e1e76f69309de1fdbc6e110c7a28a1b9caa029 - languageName: node - linkType: hard - "@storybook/blocks@npm:7.4.3": version: 7.4.3 resolution: "@storybook/blocks@npm:7.4.3" @@ -4003,7 +3971,7 @@ __metadata: languageName: node linkType: hard -"@storybook/components@npm:7.4.3, @storybook/components@npm:^7.0.0": +"@storybook/components@npm:7.4.3": version: 7.4.3 resolution: "@storybook/components@npm:7.4.3" dependencies: @@ -4065,7 +4033,7 @@ __metadata: languageName: node linkType: hard -"@storybook/core-events@npm:7.4.3, @storybook/core-events@npm:^7.0.0": +"@storybook/core-events@npm:7.4.3": version: 7.4.3 resolution: "@storybook/core-events@npm:7.4.3" dependencies: @@ -4498,7 +4466,7 @@ __metadata: languageName: node linkType: hard -"@storybook/theming@npm:7.4.3, @storybook/theming@npm:^7.0.0, @storybook/theming@npm:^7.3.2": +"@storybook/theming@npm:7.4.3, @storybook/theming@npm:^7.3.2": version: 7.4.3 resolution: "@storybook/theming@npm:7.4.3" dependencies: @@ -7968,7 +7936,6 @@ __metadata: react-select: ^5.7.2 start-server-and-test: 1.14.0 storybook: ^7.0.6 - storybook-dark-mode: ^3.0.1 stylelint: 14.15.0 stylelint-config-prettier: 9.0.4 stylelint-config-standard: 29.0.0 @@ -15368,30 +15335,6 @@ display-element-css@cfpb/storybook-addon-display-element-css: languageName: node linkType: hard -"storybook-dark-mode@npm:^3.0.1": - version: 3.0.1 - resolution: "storybook-dark-mode@npm:3.0.1" - dependencies: - "@storybook/addons": ^7.0.0 - "@storybook/api": ^7.0.0 - "@storybook/components": ^7.0.0 - "@storybook/core-events": ^7.0.0 - "@storybook/global": ^5.0.0 - "@storybook/theming": ^7.0.0 - fast-deep-equal: ^3.1.3 - memoizerific: ^1.11.3 - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 - peerDependenciesMeta: - react: - optional: true - react-dom: - optional: true - checksum: d04213c92e8a4af0035e80eb02b75b8da725ba7b1ecbfe050eb04cb4018d91394f08c8fe7c1b106c971b2047ef5a1ba776e78050ae1f6d7563cdfdba5e701a29 - languageName: node - linkType: hard - "storybook@npm:^7.0.6": version: 7.4.3 resolution: "storybook@npm:7.4.3"