diff --git a/packages/epo-react-lib/.storybook/main.js b/packages/epo-react-lib/.storybook/main.js index ff311004..cb3031fd 100644 --- a/packages/epo-react-lib/.storybook/main.js +++ b/packages/epo-react-lib/.storybook/main.js @@ -25,6 +25,7 @@ const config = { alias: { "@/assets": path.resolve(__dirname, "../src/assets"), "@/atomic": path.resolve(__dirname, "../src/atomic"), + "@/content-blocks": path.resolve(__dirname, "../src/content-blocks"), "@/contexts": path.resolve(__dirname, "../src/contexts"), "@/form": path.resolve(__dirname, "../src/form"), "@/helpers": path.resolve(__dirname, "../src/helpers"), diff --git a/packages/epo-react-lib/jest.config.ts b/packages/epo-react-lib/jest.config.ts index a2dc4d9b..f6da22af 100644 --- a/packages/epo-react-lib/jest.config.ts +++ b/packages/epo-react-lib/jest.config.ts @@ -8,6 +8,7 @@ const config: JestConfigWithTsJest = { moduleNameMapper: { "^@/assets(.*)$": "/src/assets$1", "^@/atomic(.*)$": "/src/atomic$1", + "^@/content-blocks(.*)$": "/src/content-blocks$1", "^@/contexts(.*)$": "/src/contexts$1", "^@/form(.*)$": "/src/form$1", "^@/helpers(.*)$": "/src/helpers$1", diff --git a/packages/epo-react-lib/package.json b/packages/epo-react-lib/package.json index f49b0ef4..e1128603 100644 --- a/packages/epo-react-lib/package.json +++ b/packages/epo-react-lib/package.json @@ -1,6 +1,6 @@ { "name": "@rubin-epo/epo-react-lib", - "version": "1.0.11", + "version": "1.0.12", "author": "Rubin EPO", "license": "MIT", "homepage": "https://lsst-epo.github.io/epo-react-lib", diff --git a/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.stories.tsx b/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.stories.tsx new file mode 100644 index 00000000..0e638549 --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.stories.tsx @@ -0,0 +1,140 @@ +import { ComponentMeta, ComponentStoryObj } from "@storybook/react"; + +import ComplexTable from "."; +import { ComplexTableRow } from "./ComplexTable"; + +const MockTableContent: ComplexTableRow[] = [ + { + tableRow: [ + { + id: "0VvOOc", + cellContent: "Header", + }, + { + id: "3MeJAm", + cellContent: + "Header", + }, + { + id: "fD8HUc", + cellContent: "Header", + }, + { + id: "C5l39F", + cellContent: + "Header", + }, + ], + }, + { + tableRow: [ + { + id: "KsKZtU", + cellContent: "Rubin Observatory", + }, + { + id: "OVp7Dp", + cellContent: + "Rubin Observatory", + }, + { + id: "QRm6AM", + cellContent: "Rubin Observatory", + }, + { + id: "0S8OAG", + cellContent: "Rubin Observatory", + }, + ], + }, +]; + +const meta: ComponentMeta = { + component: ComplexTable, + argTypes: { + complexTable: { + description: + "Array of `ComplexTableRow` objects to populate the table with.", + }, + plainText: { + type: "string", + description: + "Table caption shown above the table. Does not display if `isChild` is set.", + table: { + type: { + summary: "string", + }, + }, + }, + isChild: { + type: "boolean", + description: + "If the table is a child of another component, this prop will add a narrow width container around the table.", + table: { + type: { + summary: "boolean", + }, + defaultValue: { + summary: false, + }, + }, + }, + styleAs: { + type: "string", + control: { + type: "select", + }, + options: ["primary", "secondary"], + description: + "Determines if table is visually styled using primary or secondary theme.", + table: { + type: { + summary: "primary | secondary", + }, + defaultValue: { + summary: "primary", + }, + }, + }, + verticalAlignment: { + type: "string", + description: + "Sets the `vertical-align` CSS property for all table cells.", + table: { + type: { + summary: "string", + }, + defaultValue: { + summary: "top", + }, + }, + }, + }, +}; +export default meta; + +export const Primary: ComponentStoryObj = { + args: { + complexTable: MockTableContent, + styleAs: "primary", + plainText: "Complex Table", + isChild: false, + }, +}; + +export const Secondary: ComponentStoryObj = { + args: { + complexTable: MockTableContent, + styleAs: "secondary", + plainText: "Complex Table", + isChild: false, + }, +}; + +export const Child: ComponentStoryObj = { + args: { + complexTable: MockTableContent, + plainText: "Complex Table", + isChild: true, + }, +}; diff --git a/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.test.tsx b/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.test.tsx new file mode 100644 index 00000000..9906e5b8 --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.test.tsx @@ -0,0 +1,69 @@ +import { render, screen } from "@testing-library/react"; +import ComplexTable from "."; + +const props = { + plainText: "Test table", + complexTable: [ + { + tableRow: [ + { + id: "0VvOOc", + cellContent: "Header", + }, + { + id: "3MeJAm", + cellContent: + "Header", + }, + { + id: "fD8HUc", + cellContent: "Header", + }, + { + id: "C5l39F", + cellContent: + "Header", + }, + ], + }, + { + tableRow: [ + { + id: "KsKZtU", + cellContent: "Rubin Observatory", + }, + { + id: "OVp7Dp", + cellContent: + "Rubin Observatory", + }, + { + id: "QRm6AM", + cellContent: "Rubin Observatory", + }, + { + id: "0S8OAG", + cellContent: "Rubin Observatory", + }, + ], + }, + ], +}; + +describe("ComplexTable", () => { + it("should create a table with a caption", () => { + render(); + + const table = screen.getByRole("table"); + + expect(table).toBeInTheDocument; + expect(table).toHaveAccessibleName; + }); + it("should render rows", () => { + render(); + + const rows = screen.getAllByRole("row"); + + expect(rows.length).toBe(props.complexTable.length); + }); +}); diff --git a/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.tsx b/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.tsx new file mode 100644 index 00000000..bcf2790b --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/ComplexTable/ComplexTable.tsx @@ -0,0 +1,70 @@ +import { FunctionComponent } from "react"; +import Container from "@/layout/Container"; +import * as Styled from "./styles"; + +interface ComplexTableCell { + id: string; + cellBackground?: string; + hasFlexibleCellWidth?: boolean; + cellWidth?: number; + cellContent: string; +} + +export interface ComplexTableRow { + tableRow: ComplexTableCell[]; +} + +export interface ComplexTableProps { + complexTable: ComplexTableRow[]; + plainText?: string; + verticalAlignment?: string; + styleAs?: "primary" | "secondary"; + isChild?: boolean; +} + +const ComplexTable: FunctionComponent = ({ + complexTable, + plainText, + verticalAlignment, + styleAs = "primary", + isChild = false, +}) => { + const renderTable = () => ( + + + {plainText && ( + {plainText} + )} + + {complexTable.map((row, i) => ( + + {row.tableRow.map((cell) => ( + + ))} + + ))} + + + + ); + + return isChild ? ( + renderTable() + ) : ( + {renderTable()} + ); +}; + +export default ComplexTable; diff --git a/packages/epo-react-lib/src/content-blocks/ComplexTable/index.ts b/packages/epo-react-lib/src/content-blocks/ComplexTable/index.ts new file mode 100644 index 00000000..79dbfd1f --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/ComplexTable/index.ts @@ -0,0 +1 @@ +export { default } from "./ComplexTable"; diff --git a/packages/epo-react-lib/src/content-blocks/ComplexTable/styles.ts b/packages/epo-react-lib/src/content-blocks/ComplexTable/styles.ts new file mode 100644 index 00000000..0c084c62 --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/ComplexTable/styles.ts @@ -0,0 +1,70 @@ +import styled, { css } from "styled-components"; +import { fluidScale } from "@/styles/globalStyles"; +import { aHidden } from "@/styles/mixins/appearance"; + +export const TableWrapper = styled.div` + max-width: 100vw; + width: 100%; + overflow: auto; +`; + +interface TableProps { + $styleAs?: "primary" | "secondary"; + $verticalAlignment?: string; +} + +export const Table = styled.table` + width: 100%; + border-collapse: collapse; + + ${({ $styleAs, $verticalAlignment = "top" }) => + $styleAs === "secondary" + ? css` + --ComplexTable-cell-bg: var(--neutral10); + --ComplexTable-border: 5px solid var(--white); + --ComplexTable-vertical-align: ${$verticalAlignment}; + border-style: hidden; + ` + : css` + --ComplexTable-border: 1px solid var(--black); + --ComplexTable-cell-bg: none; + --ComplexTable-vertical-align: ${$verticalAlignment}; + `} +`; + +export const Caption = styled.caption<{ isChild: boolean }>` + padding-block-end: 1em; + font-size: 1.136em; + font-weight: bold; + text-align: start; + + ${({ isChild }) => (isChild ? aHidden : null)} +`; + +export const TableRow = styled.tr` + &:nth-child(odd) { + background-color: var(--neutral10); + } +`; + +interface TableCellProps { + $background?: string; + $hasFlexibleCellWidth?: boolean; + $row: number; +} + +export const TableCell = styled.td` + ${({ $background }) => + css` + background-color: ${$background || "var(--ComplexTable-cell-bg)"}; + color: ${$background ? "var(--black)" : "inherit"}; + `}; + ${({ $hasFlexibleCellWidth }) => + $hasFlexibleCellWidth && `white-space: nowrap;`} + + min-width: ${fluidScale("180px", "110px")}; + border: var(--ComplexTable-border); + padding: 20px; + text-align: inherit; + vertical-align: var(--ComplexTable-vertical-align); +`; diff --git a/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.stories.tsx b/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.stories.tsx new file mode 100644 index 00000000..8cdd7dbb --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.stories.tsx @@ -0,0 +1,51 @@ +import { ComponentMeta, ComponentStoryObj } from "@storybook/react"; + +import SimpleTable from "."; +import { SimpleTableRow } from "./SimpleTable"; + +const MockTableContent: SimpleTableRow[] = [ + { + rowColor: "none", + rowTitle: "Default background", + rowContent: + "

Circumpolar black hole syzygy dwarf star wavelength totality meridian ice giant free fall nadir parsec waning day

Rubin Observatory", + }, + { + rowColor: "blue", + rowTitle: "Blue background", + rowContent: + "

eclipse muttnik exoplanet moon plane of the ecliptic conjunction Pluto

Rubin Observatory", + }, + { + rowColor: "green", + rowTitle: "Green background", + rowContent: + "

gravity gravitational lens retrograde cluster gibbous moon occultation

Rubin Observatory", + }, + { + rowColor: "orange", + rowTitle: "Orange background", + rowContent: + "

celestial equator kiloparsec Messier object celestial binary star

Rubin Observatory", + }, + { + rowColor: "paleOrange", + rowTitle: "Pale orange background", + rowContent: + "

hypernova translunar sky lens Kirkwood gaps meteor gravitation singularity new moon density culmination event horizon north star opposition Earthshine axial tilt solar

Rubin Observatory", + }, +]; + +const meta: ComponentMeta = { + component: SimpleTable, + argTypes: { + simpleTable: {}, + }, +}; +export default meta; + +export const Primary: ComponentStoryObj = { + args: { + simpleTable: MockTableContent, + }, +}; diff --git a/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.test.tsx b/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.test.tsx new file mode 100644 index 00000000..10ebb7ad --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from "@testing-library/react"; +import SimpleTable from "."; +import { SimpleTableProps } from "./SimpleTable"; + +const props: SimpleTableProps = { + simpleTable: [ + { + rowColor: "none", + rowTitle: "Default background", + rowContent: + "

Circumpolar black hole syzygy dwarf star wavelength totality meridian ice giant free fall nadir parsec waning day

Rubin Observatory", + }, + { + rowColor: "blue", + rowTitle: "Blue background", + rowContent: + "

eclipse muttnik exoplanet moon plane of the ecliptic conjunction Pluto

Rubin Observatory", + }, + { + rowColor: "green", + rowTitle: "Green background", + rowContent: + "

gravity gravitational lens retrograde cluster gibbous moon occultation

Rubin Observatory", + }, + { + rowColor: "orange", + rowTitle: "Orange background", + rowContent: + "

celestial equator kiloparsec Messier object celestial binary star

Rubin Observatory", + }, + { + rowColor: "paleOrange", + rowTitle: "Pale orange background", + rowContent: + "

hypernova translunar sky lens Kirkwood gaps meteor gravitation singularity new moon density culmination event horizon north star opposition Earthshine axial tilt solar

Rubin Observatory", + }, + ], +}; + +describe("SimpleTable", () => { + it("should create a description list", () => { + render(); + + const table = screen.getByRole("list"); + + expect(table).toBeInTheDocument; + }); + it("should render a list item for each row", () => { + render(); + + const rows = screen.getAllByRole("term"); + + expect(rows.length).toBe(props.simpleTable.length); + }); +}); diff --git a/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.tsx b/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.tsx new file mode 100644 index 00000000..7de3edd2 --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/SimpleTable/SimpleTable.tsx @@ -0,0 +1,38 @@ +import React, { FunctionComponent, Fragment } from "react"; +import Container from "@/layout/Container"; +import * as Styled from "./styles"; + +export interface SimpleTableRow { + rowColor: Styled.SimpleTableColor; + rowTitle: string; + rowContent: string; +} + +export interface SimpleTableProps { + simpleTable: SimpleTableRow[]; +} + +const SimpleTable: FunctionComponent = ({ simpleTable }) => { + return simpleTable ? ( + + + {simpleTable.map((row, i) => ( + + + + + ))} + + + ) : null; +}; + +export default SimpleTable; diff --git a/packages/epo-react-lib/src/content-blocks/SimpleTable/index.ts b/packages/epo-react-lib/src/content-blocks/SimpleTable/index.ts new file mode 100644 index 00000000..3b6a120d --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/SimpleTable/index.ts @@ -0,0 +1 @@ +export { default } from "./SimpleTable"; diff --git a/packages/epo-react-lib/src/content-blocks/SimpleTable/styles.ts b/packages/epo-react-lib/src/content-blocks/SimpleTable/styles.ts new file mode 100644 index 00000000..955694c3 --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/SimpleTable/styles.ts @@ -0,0 +1,76 @@ +import { respond, tokens } from "@/styles/globalStyles"; +import styled, { css } from "styled-components"; +import { cContentRte } from "@/content-blocks/styles"; + +const colors = { + none: tokens.neutral10, + blue: tokens.blue20, + green: tokens.green05, + orange: tokens.orange10, + paleOrange: tokens.orange04, +}; + +// a11y link colors +const linkColors = { + none: tokens.turquoise80, + blue: tokens.turquoise90, + green: tokens.turquoise90, + orange: tokens.turquoise80, + paleOrange: tokens.turquoise80, +}; + +export type SimpleTableColor = + | "none" + | "blue" + | "green" + | "orange" + | "paleOrange"; + +interface SimpleTableColorProps { + $color: SimpleTableColor; +} + +export const List = styled.dl` + display: grid; + grid-template-columns: 212px 1fr; + gap: 5px; + + ${respond(`grid-template-columns: 1fr;`, tokens.BREAK_PHABLET)} +`; + +const accessibleLink = css` + a:not([class^="c-"]) { + color: var(--SimpleTable-link-color, ${linkColors.orange}); + text-decoration: underline; + } +`; + +export const Title = styled.dt` + ${cContentRte} + ${accessibleLink} + + padding: 20px; + padding-inline-end: 6px; + padding-block-end: 22px; + + ${({ $color = "none" }) => + css` + background: ${colors[$color]}; + --SimpleTable-link-color: ${linkColors[$color]}; + `} +`; + +export const Description = styled.dd` + ${cContentRte} + ${accessibleLink} + + + padding: 20px; + padding-block-end: 22px; + + ${({ $color = "none" }) => + css` + background: ${colors[$color]}; + --SimpleTable-link-color: ${linkColors[$color]}; + `} +`; diff --git a/packages/epo-react-lib/src/content-blocks/styles.ts b/packages/epo-react-lib/src/content-blocks/styles.ts new file mode 100644 index 00000000..fbf7a749 --- /dev/null +++ b/packages/epo-react-lib/src/content-blocks/styles.ts @@ -0,0 +1,50 @@ +import { css } from "styled-components"; +import { colorTokens, ptToEm } from "@/styles/globalStyles"; + +export const cContentRte = css` + > * + * { + margin-top: 1rem; + } + + > *:first-child { + margin-block-start: 0; + } + + a:not([class^="c-"]) { + color: var(--link-color, ${colorTokens["turquoise80"]}); + text-decoration: underline; + } + + ul { + padding-left: 1em; + list-style-type: disc; + } + + ol { + padding-left: 1em; + list-style-type: decimal; + } + + li { + padding-left: 0.5em; + } + + h1 { + margin-block-start: ${ptToEm("144pt")}; + } + + h2 { + margin-block-start: ${ptToEm("40pt")}; + } + + h3, + h4 { + margin-block-start: ${ptToEm("20pt")}; + } + + figcaption { + font-size: 18px; + padding-block-start: 1em; + padding-block-end: 1em; + } +`; diff --git a/packages/epo-react-lib/src/index.ts b/packages/epo-react-lib/src/index.ts index 6e12c0d2..e39bc92e 100644 --- a/packages/epo-react-lib/src/index.ts +++ b/packages/epo-react-lib/src/index.ts @@ -1,29 +1,43 @@ +// styled-components + export { default as GlobalStyles } from "@/styles/globalStyles"; + +// i18n export { default as localeStrings } from "@/assets/locales"; +// Atomic export { default as Accordion } from "@/atomic/Accordion"; +export { default as Button } from "@/atomic/Button"; +export { default as Buttonish } from "@/atomic/Buttonish"; +export { default as ExpandToggle } from "@/atomic/ExpandToggle"; export { default as ExternalLink } from "@/atomic/ExternalLink"; -export { default as Link } from "@/atomic/Link"; export { default as Figure } from "@/atomic/Figure"; export { default as Image } from "@/atomic/Image"; +export { default as Link } from "@/atomic/Link"; +export { default as MixedLink } from "@/atomic/MixedLink"; export { default as ResponsiveImage } from "@/atomic/ResponsiveImage"; -export { default as IconComposer } from "@/svg/IconComposer"; -export { default as icons } from "@/svg/icons"; -export { default as Button } from "@/atomic/Button"; -export { default as ExpandToggle } from "@/atomic/Button"; +export * from "@/atomic/Share"; export { default as Video } from "@/atomic/Video"; -export { default as CarouselLayout } from "@/layout/Carousel"; + +// Content Blocks +export { default as SimpleTable } from "@/content-blocks/SimpleTable"; + +// Form export { default as Error } from "@/form/Error"; export { default as FormButtons } from "@/form/FormButtons"; export { default as FormField } from "@/form/FormField"; export { default as Input, Password } from "@/form/Input"; export { default as Select } from "@/form/Select"; export { default as Switch } from "@/form/Switch"; -export { default as Buttonish } from "@/atomic/Buttonish"; -export { default as MixedLink } from "@/atomic/MixedLink"; -export * from "@/atomic/Share"; + +// Layout +export { default as BasicModal } from "@/layout/BasicModal"; +export { default as CarouselLayout } from "@/layout/Carousel"; export { default as Columns } from "@/layout/Columns"; export { default as Container } from "@/layout/Container"; export { default as Grid } from "@/layout/Grid"; export { default as MasonryGrid } from "@/layout/MasonryGrid"; -export { default as BasicModal } from "@/layout/BasicModal"; + +// SVG +export { default as IconComposer } from "@/svg/IconComposer"; +export { default as icons } from "@/svg/icons"; diff --git a/packages/epo-react-lib/tsconfig.json b/packages/epo-react-lib/tsconfig.json index d1cadbe3..2d3a78b4 100644 --- a/packages/epo-react-lib/tsconfig.json +++ b/packages/epo-react-lib/tsconfig.json @@ -20,6 +20,7 @@ "paths": { "@/assets/*": ["src/assets/*"], "@/atomic/*": ["src/atomic/*"], + "@/content-blocks/*": ["src/content-blocks/*"], "@/contexts/*": ["src/contexts/*"], "@/form/*": ["src/form/*"], "@/helpers/*": ["src/helpers/*"], @@ -34,6 +35,5 @@ } }, "include": ["src", "./jest-setup.ts"], - // "exclude": ["**/*.test.*", "**/__mocks__/*", "**/__tests__/*"], "references": [{ "path": "./tsconfig.node.json" }] } diff --git a/packages/epo-react-lib/vite.config.ts b/packages/epo-react-lib/vite.config.ts index 441b44a3..2c80a3e2 100644 --- a/packages/epo-react-lib/vite.config.ts +++ b/packages/epo-react-lib/vite.config.ts @@ -38,6 +38,7 @@ export default defineConfig({ alias: { "@/assets": resolve(__dirname, "./src/assets"), "@/atomic": resolve(__dirname, "./src/atomic"), + "@/content-blocks": resolve(__dirname, "./src/content-blocks"), "@/contexts": resolve(__dirname, "./src/contexts"), "@/form": resolve(__dirname, "./src/form"), "@/helpers": resolve(__dirname, "./src/helpers"),