Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
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
1 change: 1 addition & 0 deletions .lastsync
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
9014da9bbe05bd45f38841c02e34dcb5a8e3e1d8
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openstax/ui-components",
"version": "1.18.6",
"version": "1.18.7",
"license": "MIT",
"source": "./src/index.ts",
"types": "./dist/index.d.ts",
Expand Down Expand Up @@ -33,11 +33,11 @@
"styled-components": "*"
},
"devDependencies": {
"npm-run-all": "^4.1.5",
"@ladle/react": "^2.1.2",
"@openstax/ts-utils": "^1.27.6",
"@openstax/ts-utils": "^1.32.5",
"@playwright/test": "^1.25.0",
"@testing-library/dom": "^10.4.0",
"@types/dompurify": "^3.0.0",
"@testing-library/jest-dom": "^6.4.8",
"@testing-library/react": "^12.0.0",
"@testing-library/user-event": "^14.5.2",
Expand All @@ -59,6 +59,7 @@
"jest-environment-node": "^29.6.2",
"microbundle": "^0.15.1",
"node-fetch": "<3.0.0",
"npm-run-all": "^4.1.5",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-is": "^16.8.0",
Expand All @@ -72,6 +73,7 @@
"@sentry/react": "^7.120.3",
"classnames": "^2.3.1",
"crypto": "npm:crypto-browserify@^3.12.0",
"dompurify": "^3.0.1",
"react-aria": "^3.37.0",
"react-aria-components": "1.10.1",
"stream": "npm:stream-browserify@^3.0.0"
Expand Down
33 changes: 33 additions & 0 deletions src/components/Banner/Banner.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Banner } from "./Banner";
import renderer from 'react-test-renderer';

describe('Banner', () => {
it('matches snapshot (single message, no dismiss)', () => {
const tree = renderer.create(
<Banner messages={['This is a note']} severity='note' />
).toJSON();
expect(tree).toMatchSnapshot();
});

it('matches snapshot (multiple messages, with dismiss)', () => {
const tree = renderer.create(
<Banner
messages={['This is warning one', 'This is warning two']}
severity='warning'
onDismiss={() => () => alert('dismiss checkout')}
/>
).toJSON();
expect(tree).toMatchSnapshot();
});

it('matches snapshot (error, with dismiss)', () => {
const tree = renderer.create(
<Banner
messages={['This is an error']}
severity='error'
onDismiss={() => () => alert('dismiss checkout')}
/>
).toJSON();
expect(tree).toMatchSnapshot();
});
});
15 changes: 15 additions & 0 deletions src/components/Banner/Banner.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import { Banner } from './Banner';

export const Error = () => <Banner messages={['This is an error message']} severity='error' />;

export const Warning = () => <Banner messages={['This is a warning message']} severity='warning' />;

export const Note = () => <Banner messages={['This is a note message']} severity='note' />;

export const MultipleMessages = () => <Banner messages={['First message', 'Second message', 'Third message']} severity='warning' />;

export const Dismissible = () => {
const [visible, setVisible] = React.useState(true);
return visible ? <Banner messages={['This is a dismissible warning message']} severity='warning' onDismiss={() => setVisible(false)} /> : null;
};
76 changes: 76 additions & 0 deletions src/components/Banner/Banner.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { DismissIcon } from "../svgs/DismissIcon";
import { Html } from "../Html";
import styled from 'styled-components';
import { Button, ButtonLink } from '../Button';
import { colors } from '../../theme';

export type BannerSeverity = 'note' | 'warning' | 'error';

export const Severity = styled.span`
font-weight: bold;
text-transform: uppercase;
`;

export const StyledBanner = styled.div<{severity: BannerSeverity}>`
position: relative;
background: ${({severity}) => severity === 'error' ? '#F8E8EA' : '#fff5e0'};
color: ${({severity}) => severity === 'error' ? colors.palette.darkRed : '#976502'};
border: ${({severity}) => severity === 'error' ? `1px solid ${colors.palette.lightRed}` : '1px solid #fdbd3e'};
padding: .6rem 1.6rem;
margin: 0 0 1.6rem 0;
line-height: 2rem;
display: flex;
align-items: center;

a {
text-decoration: none;
color: ${colors.palette.mediumBlue};

&:hover {
text-decoration: underline;
color: ${colors.link.hover}
}
}

${ButtonLink} {
font-size: 1.6rem;
}
`;

export const CloseButton = styled(Button)<{severity: BannerSeverity}>`
color: ${({severity}) => severity === 'error' ? colors.palette.darkRed : '#976502'};
overflow: visible;
background: none;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
box-shadow: none;
margin-left: 2.4rem;

&:not([disabled]):hover,
&:not([disabled]):active {
background: none;
}
`;

export const Banner = (props: {messages: string[]; severity: BannerSeverity; onDismiss?: () => void}) => {
const numWarnings = props.messages.length;

return <StyledBanner severity={props.severity}>
<div>
{props.severity !== 'error' ? <Severity>{props.severity === 'note' ? 'Note: ' : 'Warning: '}</Severity> : null}
{props.messages.map((message, i) =>
<Html block={numWarnings > 1} key={i}>
{numWarnings > 1 ? `[${i + 1} of ${numWarnings}]: ${message}`: message}
</Html>
)}
</div>
{props.onDismiss
? <CloseButton severity={props.severity} onClick={props.onDismiss} aria-label='dismiss'>
<DismissIcon aria-hidden='true' focusable='false' />
</CloseButton>
: null}
</StyledBanner>;
};
136 changes: 136 additions & 0 deletions src/components/Banner/__snapshots__/Banner.spec.tsx.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`Banner matches snapshot (error, with dismiss) 1`] = `
<div
className="sc-jSMfEi iGEqwr"
>
<div>
<span
dangerouslySetInnerHTML={
Object {
"__html": "This is an error",
}
}
/>
</div>
<button
aria-label="dismiss"
className="sc-bczRLJ sc-gKXOVf dOOknK dwyZcw"
onClick={[Function]}
severity="error"
>
<svg
aria-hidden="true"
focusable="false"
height="15px"
version="1.1"
viewBox="0 0 15 15"
width="15px"
>
<g
fill="none"
fillRule="evenodd"
stroke="none"
strokeWidth="1"
>
<g
fill="currentColor"
transform="translate(-302.000000, -18.000000)"
>
<g
transform="translate(302.000000, 18.000000)"
>
<path
d="M7.5,5.41522791 L12.0331524,0.579865364 C12.3077536,0.286957429 12.7165503,0.24816296 12.946282,0.493210121 L13.9861449,1.60239723 C14.2158766,1.84744439 14.1795068,2.28349422 13.9049056,2.57640216 L9.37175324,7.41176471 L13.9049056,12.2471273 C14.1795068,12.5400352 14.2158766,12.976085 13.9861449,13.2211322 L12.946282,14.3303193 C12.7165503,14.5753665 12.3077536,14.536572 12.0331524,14.243664 L7.5,9.4083015 L2.96684761,14.243664 C2.69224642,14.536572 2.2834497,14.5753665 2.05371799,14.3303193 L1.01385508,13.2211322 C0.784123363,12.976085 0.820493178,12.5400352 1.09509437,12.2471273 L5.62824676,7.41176471 L1.09509437,2.57640216 C0.820493178,2.28349422 0.784123363,1.84744439 1.01385508,1.60239723 L2.05371799,0.493210121 C2.2834497,0.24816296 2.69224642,0.286957429 2.96684761,0.579865364 L7.5,5.41522791 Z"
/>
</g>
</g>
</g>
</svg>
</button>
</div>
`;

exports[`Banner matches snapshot (multiple messages, with dismiss) 1`] = `
<div
className="sc-jSMfEi lmuhoy"
>
<div>
<span
className="sc-eCYdqJ EIRbC"
>
Warning:
</span>
<div
dangerouslySetInnerHTML={
Object {
"__html": "[1 of 2]: This is warning one",
}
}
/>
<div
dangerouslySetInnerHTML={
Object {
"__html": "[2 of 2]: This is warning two",
}
}
/>
</div>
<button
aria-label="dismiss"
className="sc-bczRLJ sc-gKXOVf dOOknK fGvwkl"
onClick={[Function]}
severity="warning"
>
<svg
aria-hidden="true"
focusable="false"
height="15px"
version="1.1"
viewBox="0 0 15 15"
width="15px"
>
<g
fill="none"
fillRule="evenodd"
stroke="none"
strokeWidth="1"
>
<g
fill="currentColor"
transform="translate(-302.000000, -18.000000)"
>
<g
transform="translate(302.000000, 18.000000)"
>
<path
d="M7.5,5.41522791 L12.0331524,0.579865364 C12.3077536,0.286957429 12.7165503,0.24816296 12.946282,0.493210121 L13.9861449,1.60239723 C14.2158766,1.84744439 14.1795068,2.28349422 13.9049056,2.57640216 L9.37175324,7.41176471 L13.9049056,12.2471273 C14.1795068,12.5400352 14.2158766,12.976085 13.9861449,13.2211322 L12.946282,14.3303193 C12.7165503,14.5753665 12.3077536,14.536572 12.0331524,14.243664 L7.5,9.4083015 L2.96684761,14.243664 C2.69224642,14.536572 2.2834497,14.5753665 2.05371799,14.3303193 L1.01385508,13.2211322 C0.784123363,12.976085 0.820493178,12.5400352 1.09509437,12.2471273 L5.62824676,7.41176471 L1.09509437,2.57640216 C0.820493178,2.28349422 0.784123363,1.84744439 1.01385508,1.60239723 L2.05371799,0.493210121 C2.2834497,0.24816296 2.69224642,0.286957429 2.96684761,0.579865364 L7.5,5.41522791 Z"
/>
</g>
</g>
</g>
</svg>
</button>
</div>
`;

exports[`Banner matches snapshot (single message, no dismiss) 1`] = `
<div
className="sc-jSMfEi lmuhoy"
>
<div>
<span
className="sc-eCYdqJ EIRbC"
>
Note:
</span>
<span
dangerouslySetInnerHTML={
Object {
"__html": "This is a note",
}
}
/>
</div>
</div>
`;
4 changes: 2 additions & 2 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,12 @@ interface ButtonOptions {
type ButtonBase = React.ComponentPropsWithoutRef<'button'> & ButtonOptions;
type LinkButtonBase = React.ComponentPropsWithoutRef<'a'> & ButtonOptions;

interface ButtonProps extends ButtonBase {
export interface ButtonProps extends ButtonBase {
isWaiting?: never;
waitingText?: never;
}

interface WaitingButtonProps extends ButtonBase {
export interface WaitingButtonProps extends ButtonBase {
isWaiting: boolean;
waitingText: string;
}
Expand Down
2 changes: 1 addition & 1 deletion src/components/Error.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React from "react";
import * as Sentry from '@sentry/react';
import { BoxBody, BoxHeading, BoxEventId } from "./MessageBox.styles";
import { BoxBody, BoxHeading, BoxEventId } from "./MessageBox/MessageBox";
import { ErrorContext } from "../contexts";

export interface ErrorPropTypes {
Expand Down
40 changes: 40 additions & 0 deletions src/components/Html.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { Html } from "./Html";
import { render } from '@testing-library/react';

describe('Html', () => {
let root: HTMLElement;

beforeEach(() => {
root = document.createElement('main');
root.id = 'root';
document.body.append(root);
});

it('matches snapshot (Block)', () => {
render(
<Html block className="custom-class">
This is a block of HTML
</Html>
);

expect(document.body).toMatchSnapshot();
});

it('matches snapshot (Inline)', () => {
render(
<Html className="custom-class">
This is an inline HTML.
</Html>
);

expect(document.body).toMatchSnapshot();
});

it('matches snapshot (Empty)', () => {
render(<Html />);

expect(document.body).toMatchSnapshot();
});
});


11 changes: 11 additions & 0 deletions src/components/Html.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Html } from "./Html";

export const Block = () => <Html block className="custom-class">
This is a block of HTML
</Html>;

export const Inline = () => <Html className="custom-class">
This is an inline HTML.
</Html>;

export const Empty = () => <Html />; // renders nothing
14 changes: 14 additions & 0 deletions src/components/Html.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import { assertString } from "@openstax/ts-utils/assertions";
import DOMPurify from 'dompurify';

export const Html = (props: React.PropsWithChildren<{block?: boolean; className?: string}>) => {
if (props.children === undefined) {
return null;
}
const html = DOMPurify.sanitize(assertString(props.children), {ADD_ATTR: ['target']});
return props.block
? <div dangerouslySetInnerHTML={{__html: html}} className={props.className} />
: <span dangerouslySetInnerHTML={{__html: html}} className={props.className} />
;
};
Loading
Loading