Skip to content

Commit c635279

Browse files
committed
Merge remote-tracking branch 'origin/main' into fix-radio-buttons
2 parents eb843c0 + 80cba97 commit c635279

30 files changed

+2485
-859
lines changed

.github/workflows/checks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ jobs:
1010
name: Jira
1111
runs-on: ubuntu-latest
1212
steps:
13-
- uses: openstax/jira-linked-action@v0.1.14
13+
- uses: openstax/jira-linked-action@v0.1.15
1414
with:
1515
jira_site: openstax
1616
jira_project: DISCO

.lastsync

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
9014da9bbe05bd45f38841c02e34dcb5a8e3e1d8

package.json

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@openstax/ui-components",
3-
"version": "1.18.5",
3+
"version": "1.19.0",
44
"license": "MIT",
55
"source": "./src/index.ts",
66
"types": "./dist/index.d.ts",
@@ -33,13 +33,14 @@
3333
"styled-components": "*"
3434
},
3535
"devDependencies": {
36-
"npm-run-all": "^4.1.5",
3736
"@ladle/react": "^2.1.2",
38-
"@openstax/ts-utils": "^1.27.6",
37+
"@openstax/ts-utils": "^1.32.5",
3938
"@playwright/test": "^1.25.0",
4039
"@testing-library/dom": "^10.4.0",
40+
"@types/dompurify": "^3.0.0",
4141
"@testing-library/jest-dom": "^6.4.8",
4242
"@testing-library/react": "^12.0.0",
43+
"@testing-library/react-hooks": "^8.0.1",
4344
"@testing-library/user-event": "^14.5.2",
4445
"@types/jest": "^28.1.4",
4546
"@types/node": "^18.7.5",
@@ -59,6 +60,7 @@
5960
"jest-environment-node": "^29.6.2",
6061
"microbundle": "^0.15.1",
6162
"node-fetch": "<3.0.0",
63+
"npm-run-all": "^4.1.5",
6264
"react": "^17.0.2",
6365
"react-dom": "^17.0.2",
6466
"react-is": "^16.8.0",
@@ -72,6 +74,7 @@
7274
"@sentry/react": "^7.120.3",
7375
"classnames": "^2.3.1",
7476
"crypto": "npm:crypto-browserify@^3.12.0",
77+
"dompurify": "^3.0.1",
7578
"react-aria": "^3.37.0",
7679
"react-aria-components": "1.10.1",
7780
"stream": "npm:stream-browserify@^3.0.0"
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Banner } from "./Banner";
2+
import renderer from 'react-test-renderer';
3+
4+
describe('Banner', () => {
5+
it('matches snapshot (single message, no dismiss)', () => {
6+
const tree = renderer.create(
7+
<Banner messages={['This is a note']} severity='note' />
8+
).toJSON();
9+
expect(tree).toMatchSnapshot();
10+
});
11+
12+
it('matches snapshot (multiple messages, with dismiss)', () => {
13+
const tree = renderer.create(
14+
<Banner
15+
messages={['This is warning one', 'This is warning two']}
16+
severity='warning'
17+
onDismiss={() => () => alert('dismiss checkout')}
18+
/>
19+
).toJSON();
20+
expect(tree).toMatchSnapshot();
21+
});
22+
23+
it('matches snapshot (error, with dismiss)', () => {
24+
const tree = renderer.create(
25+
<Banner
26+
messages={['This is an error']}
27+
severity='error'
28+
onDismiss={() => () => alert('dismiss checkout')}
29+
/>
30+
).toJSON();
31+
expect(tree).toMatchSnapshot();
32+
});
33+
});
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import React from 'react';
2+
import { Banner } from './Banner';
3+
import styled from 'styled-components';
4+
5+
const BannerContainer = styled.div`
6+
font-size: 1.2rem;
7+
position: relative;
8+
padding-right: 2.5rem;
9+
width: 42rem;
10+
11+
svg {
12+
position: absolute;
13+
top: 1.7rem;
14+
right: 1rem;
15+
cursor: pointer;
16+
}
17+
`;
18+
19+
export const Error = () => (
20+
<BannerContainer>
21+
<Banner messages={['This is an error message']} severity='error' />
22+
</BannerContainer>
23+
);
24+
25+
export const Warning = () => (
26+
<BannerContainer>
27+
<Banner messages={['This is a warning message']} severity='warning' />
28+
</BannerContainer>
29+
);
30+
31+
export const Note = () => (
32+
<BannerContainer>
33+
<Banner messages={['This is a note message']} severity='note' />
34+
</BannerContainer>
35+
);
36+
37+
export const MultipleMessages = () => (
38+
<BannerContainer>
39+
<Banner messages={['First message', 'Second message', 'Third message']} severity='warning' />
40+
</BannerContainer>
41+
);
42+
43+
export const Dismissible = () => {
44+
const [visible, setVisible] = React.useState(true);
45+
return visible ? (
46+
<BannerContainer>
47+
<Banner
48+
messages={['This is a dismissible warning message']}
49+
severity='warning'
50+
onDismiss={() => setVisible(false)}
51+
/>
52+
</BannerContainer>
53+
) : null;
54+
};

src/components/Banner/Banner.tsx

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { DismissIcon } from "../svgs/DismissIcon";
2+
import { Html } from "../Html";
3+
import styled from 'styled-components';
4+
import { Button, ButtonLink } from '../Button';
5+
import { colors } from '../../theme';
6+
7+
export type BannerSeverity = 'note' | 'warning' | 'error';
8+
9+
export const Severity = styled.span`
10+
font-weight: bold;
11+
text-transform: uppercase;
12+
`;
13+
14+
export const StyledBanner = styled.div<{severity: BannerSeverity}>`
15+
position: relative;
16+
background: ${({severity}) => severity === 'error' ? '#F8E8EA' : '#fff5e0'};
17+
color: ${({severity}) => severity === 'error' ? colors.palette.darkRed : '#976502'};
18+
border: ${({severity}) => severity === 'error' ? `1px solid ${colors.palette.lightRed}` : '1px solid #fdbd3e'};
19+
padding: .6rem 1.6rem;
20+
margin: 0 0 1.6rem 0;
21+
line-height: 2rem;
22+
display: flex;
23+
align-items: center;
24+
25+
a {
26+
text-decoration: none;
27+
color: ${colors.palette.mediumBlue};
28+
29+
&:hover {
30+
text-decoration: underline;
31+
color: ${colors.link.hover}
32+
}
33+
}
34+
35+
${ButtonLink} {
36+
font-size: 1.6rem;
37+
}
38+
`;
39+
40+
export const CloseButton = styled(Button)<{severity: BannerSeverity}>`
41+
color: ${({severity}) => severity === 'error' ? colors.palette.darkRed : '#976502'};
42+
overflow: visible;
43+
background: none;
44+
border: none;
45+
padding: 0;
46+
font: inherit;
47+
cursor: pointer;
48+
outline: inherit;
49+
box-shadow: none;
50+
margin-left: 2.4rem;
51+
52+
&:not([disabled]):hover,
53+
&:not([disabled]):active {
54+
background: none;
55+
}
56+
`;
57+
58+
export const Banner = (props: {messages: string[]; severity: BannerSeverity; onDismiss?: () => void}) => {
59+
const numWarnings = props.messages.length;
60+
61+
return <StyledBanner severity={props.severity}>
62+
<div>
63+
{props.severity !== 'error' ? <Severity>{props.severity === 'note' ? 'Note: ' : 'Warning: '}</Severity> : null}
64+
{props.messages.map((message, i) =>
65+
<Html block={numWarnings > 1} key={i}>
66+
{numWarnings > 1 ? `[${i + 1} of ${numWarnings}]: ${message}`: message}
67+
</Html>
68+
)}
69+
</div>
70+
{props.onDismiss
71+
? <CloseButton severity={props.severity} onClick={props.onDismiss} aria-label='dismiss'>
72+
<DismissIcon aria-hidden='true' focusable='false' />
73+
</CloseButton>
74+
: null}
75+
</StyledBanner>;
76+
};
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
// Jest Snapshot v1, https://goo.gl/fbAQLP
2+
3+
exports[`Banner matches snapshot (error, with dismiss) 1`] = `
4+
<div
5+
className="sc-jSMfEi iGEqwr"
6+
>
7+
<div>
8+
<span
9+
dangerouslySetInnerHTML={
10+
Object {
11+
"__html": "This is an error",
12+
}
13+
}
14+
/>
15+
</div>
16+
<button
17+
aria-label="dismiss"
18+
className="sc-bczRLJ sc-gKXOVf dOOknK dwyZcw"
19+
onClick={[Function]}
20+
severity="error"
21+
>
22+
<svg
23+
aria-hidden="true"
24+
focusable="false"
25+
height="15px"
26+
version="1.1"
27+
viewBox="0 0 15 15"
28+
width="15px"
29+
>
30+
<g
31+
fill="none"
32+
fillRule="evenodd"
33+
stroke="none"
34+
strokeWidth="1"
35+
>
36+
<g
37+
fill="currentColor"
38+
transform="translate(-302.000000, -18.000000)"
39+
>
40+
<g
41+
transform="translate(302.000000, 18.000000)"
42+
>
43+
<path
44+
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"
45+
/>
46+
</g>
47+
</g>
48+
</g>
49+
</svg>
50+
</button>
51+
</div>
52+
`;
53+
54+
exports[`Banner matches snapshot (multiple messages, with dismiss) 1`] = `
55+
<div
56+
className="sc-jSMfEi lmuhoy"
57+
>
58+
<div>
59+
<span
60+
className="sc-eCYdqJ EIRbC"
61+
>
62+
Warning:
63+
</span>
64+
<div
65+
dangerouslySetInnerHTML={
66+
Object {
67+
"__html": "[1 of 2]: This is warning one",
68+
}
69+
}
70+
/>
71+
<div
72+
dangerouslySetInnerHTML={
73+
Object {
74+
"__html": "[2 of 2]: This is warning two",
75+
}
76+
}
77+
/>
78+
</div>
79+
<button
80+
aria-label="dismiss"
81+
className="sc-bczRLJ sc-gKXOVf dOOknK fGvwkl"
82+
onClick={[Function]}
83+
severity="warning"
84+
>
85+
<svg
86+
aria-hidden="true"
87+
focusable="false"
88+
height="15px"
89+
version="1.1"
90+
viewBox="0 0 15 15"
91+
width="15px"
92+
>
93+
<g
94+
fill="none"
95+
fillRule="evenodd"
96+
stroke="none"
97+
strokeWidth="1"
98+
>
99+
<g
100+
fill="currentColor"
101+
transform="translate(-302.000000, -18.000000)"
102+
>
103+
<g
104+
transform="translate(302.000000, 18.000000)"
105+
>
106+
<path
107+
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"
108+
/>
109+
</g>
110+
</g>
111+
</g>
112+
</svg>
113+
</button>
114+
</div>
115+
`;
116+
117+
exports[`Banner matches snapshot (single message, no dismiss) 1`] = `
118+
<div
119+
className="sc-jSMfEi lmuhoy"
120+
>
121+
<div>
122+
<span
123+
className="sc-eCYdqJ EIRbC"
124+
>
125+
Note:
126+
</span>
127+
<span
128+
dangerouslySetInnerHTML={
129+
Object {
130+
"__html": "This is a note",
131+
}
132+
}
133+
/>
134+
</div>
135+
</div>
136+
`;

src/components/Button.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,12 @@ interface ButtonOptions {
4343
type ButtonBase = React.ComponentPropsWithoutRef<'button'> & ButtonOptions;
4444
type LinkButtonBase = React.ComponentPropsWithoutRef<'a'> & ButtonOptions;
4545

46-
interface ButtonProps extends ButtonBase {
46+
export interface ButtonProps extends ButtonBase {
4747
isWaiting?: never;
4848
waitingText?: never;
4949
}
5050

51-
interface WaitingButtonProps extends ButtonBase {
51+
export interface WaitingButtonProps extends ButtonBase {
5252
isWaiting: boolean;
5353
waitingText: string;
5454
}

0 commit comments

Comments
 (0)