@ndlib/component-library 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ declare const meta: Meta<typeof Card>;
4
4
  export default meta;
5
5
  type Story = StoryObj<typeof Card>;
6
6
  export declare const Default: Story;
7
+ export declare const ContainedImage: Story;
7
8
  export declare const TruncateHeadline: Story;
8
9
  export declare const Horizontal: Story;
9
10
  export declare const CustomBackgroundColor: Story;
@@ -16,7 +16,15 @@ export default meta;
16
16
  const headlinedCards = [
17
17
  {
18
18
  headline: 'Exhibit — Printing the Nation: A Century of Irish Book Arts',
19
- image: 'https://images.ctfassets.net/cfblb1f7i85j/7aCd5Sm86JdtQepGBKDUfy/dcb4d97dd3a1d3ee810e8bcaa82dc715/Spring_Exhibit_2023-Rep.jpg?w=296',
19
+ image: 'https://strapi-prod-library-website-contentbucket52d4b12c-1whgwwl6746tz.s3.us-east-1.amazonaws.com/14_15_I_Ten_American_Diaries_29a3788848.jpg',
20
+ size: CARD_SIZE.SM,
21
+ sx: {
22
+ width: '500px',
23
+ },
24
+ },
25
+ {
26
+ headline: 'Tracy C. Bergstrom, director of the Specialized Collection Services Program',
27
+ image: 'https://strapi-prod-library-website-contentbucket52d4b12c-1whgwwl6746tz.s3.us-east-1.amazonaws.com/Tracy_News_218x275_1dd180dc80.jpg',
20
28
  size: CARD_SIZE.SM,
21
29
  sx: {
22
30
  width: '500px',
@@ -24,7 +32,7 @@ const headlinedCards = [
24
32
  },
25
33
  {
26
34
  headline: 'One Book, One Michiana Digital Exhibit — Papers Alight: Contextualizing Mike Curato’s Flamer. Other text to make this longer than the other card.',
27
- image: 'https://images.ctfassets.net/cfblb1f7i85j/2sGpdDbNkl6MPnlem6wq1R/995c786624613b5ff07228e481996385/One.Book.2023-Rep.png?w=296',
35
+ image: 'https://images.ctfassets.net/cfblb1f7i85j/2sGpdDbNkl6MPnlem6wq1R/995c786624613b5ff07228e481996385/One.Book.2023-Rep.png',
28
36
  size: CARD_SIZE.SM,
29
37
  sx: {
30
38
  width: '500px',
@@ -85,6 +93,10 @@ export const Default = {
85
93
  render: () => (_jsx(Column, { children: headlinedCards.map((props, i) => (_createElement(Card, Object.assign({}, props, { key: i })))) })),
86
94
  args: {},
87
95
  };
96
+ export const ContainedImage = {
97
+ render: () => (_jsx(Column, { children: headlinedCards.map((props, i) => (_createElement(Card, Object.assign({ containImage: true, containerHeight: '280px' }, props, { key: i })))) })),
98
+ args: {},
99
+ };
88
100
  export const TruncateHeadline = {
89
101
  render: () => (_jsx(Column, { children: headlinedCards.map((props, i) => (_createElement(Card, Object.assign({}, props, { key: i, truncateHeadlineAfter: 2 })))) })),
90
102
  args: {},
@@ -1,6 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { fireEvent } from '@testing-library/react';
2
+ import { fireEvent, screen } from '@testing-library/react';
3
3
  import { vi } from 'vitest';
4
+ import { COLOR } from '../../../theme/colors';
4
5
  import { render } from '../../../utils/test';
5
6
  import { Card } from '.';
6
7
  import { KEY_CODES } from '../../../utils/misc';
@@ -34,4 +35,38 @@ describe('Card', () => {
34
35
  const { getByLabelText } = render(_jsx(Card, { heading: MOCK_HEADLINE }));
35
36
  expect(getByLabelText(MOCK_HEADLINE)).toBeDefined();
36
37
  });
38
+ it('renders the image with containImage prop', () => {
39
+ const imageSrc = 'https://via.placeholder.com/150';
40
+ const altText = 'Test Image';
41
+ const containerWidth = '500px';
42
+ const containerHeight = '280px';
43
+ render(_jsx(Card, { image: imageSrc, alt: altText, containImage: true, containerWidth: containerWidth, containerHeight: containerHeight }));
44
+ const imageElement = screen.getByAltText(altText);
45
+ expect(imageElement).toBeInTheDocument();
46
+ expect(imageElement).toHaveStyle({
47
+ width: '100%',
48
+ height: 'auto',
49
+ objectFit: 'scale-down',
50
+ });
51
+ const containerElement = imageElement.closest('div');
52
+ expect(containerElement).toHaveStyle({
53
+ width: containerWidth,
54
+ height: containerHeight,
55
+ backgroundColor: COLOR.ND_SKY_BLUE,
56
+ display: 'flex',
57
+ justifyContent: 'center',
58
+ alignItems: 'center',
59
+ });
60
+ });
61
+ it('renders the image without containImage prop', () => {
62
+ const imageSrc = 'https://via.placeholder.com/150';
63
+ const altText = 'Test Image';
64
+ render(_jsx(Card, { image: imageSrc, alt: altText }));
65
+ const imageElement = screen.getByAltText(altText);
66
+ expect(imageElement).toBeInTheDocument();
67
+ expect(imageElement).toHaveStyle({
68
+ width: '100%',
69
+ height: 'auto',
70
+ });
71
+ });
37
72
  });
@@ -32,6 +32,9 @@ export type CardProps = StyledElementProps<HTMLDivElement, {
32
32
  raised?: boolean;
33
33
  imageHeight?: string;
34
34
  imageWidth?: string;
35
+ containerWidth?: string;
36
+ containerHeight?: string;
37
+ containImage?: boolean;
35
38
  backgroundColor?: COLOR;
36
39
  }, CardChildren>;
37
40
  export declare const Card: React.FC<CardProps>;
@@ -52,7 +52,7 @@ const DateDisplay = ({ date: dateString }) => {
52
52
  mt: 1,
53
53
  } }, { children: MONTH_LABELS[date.getMonth()] }))] })));
54
54
  };
55
- export const Card = ({ size, displayDate, headline, image, alt, layout, onClick, raised, truncateHeadlineAfter, heading, headingStyles, headingIcon, headingAction, backgroundColor, sx, imageHeight: imageHeightProp, imageWidth: imageWidthProp, children, }) => {
55
+ export const Card = ({ size, displayDate, headline, image, alt, layout, onClick, raised, truncateHeadlineAfter, heading, headingStyles, headingIcon, headingAction, backgroundColor, sx, imageHeight: imageHeightProp, imageWidth: imageWidthProp, containImage, containerWidth, containerHeight, children, }) => {
56
56
  const theme = useTheme();
57
57
  const contentPaddingX = size === CARD_SIZE.SM ? 3 : 4;
58
58
  const isVertical = !layout || layout === CARD_LAYOUT.VERTICAL;
@@ -77,10 +77,21 @@ export const Card = ({ size, displayDate, headline, image, alt, layout, onClick,
77
77
  borderColor: COLOR.ND_SKY_BLUE_DARK,
78
78
  backgroundColor: COLOR.ND_SKY_BLUE,
79
79
  }
80
- : {} }, sx) }, { children: [image && (_jsx("img", { src: image, alt: alt, style: {
80
+ : {} }, sx) }, { children: [image && !containImage && (_jsx("img", { src: image, alt: alt, style: {
81
81
  width: imageWidth,
82
82
  height: imageHeight,
83
- } })), displayDate && _jsx(DateDisplay, { date: displayDate }), heading && (_jsxs(Row, Object.assign({ sx: Object.assign({ bg: COLOR.PRIMARY, color: COLOR.WHITE, width: '100%', px: contentPaddingX, py: 3, justifyContent: 'space-between', alignItems: 'center' }, headingStyles) }, { children: [_jsxs(Row, { children: [headingIcon && (_jsx(Icon, { icon: headingIcon, size: FONT_SIZE.LG, color: COLOR.WHITE, sx: { mr: 2 } })), _jsx(Heading, Object.assign({ size: HEADING_SIZE.SM, sx: {
83
+ } })), image && containImage && (_jsx(Box, Object.assign({ sx: {
84
+ width: containerWidth || '500px',
85
+ height: containerHeight || '280px',
86
+ backgroundColor: COLOR.ND_SKY_BLUE,
87
+ display: 'flex',
88
+ justifyContent: 'center',
89
+ alignItems: 'center',
90
+ } }, { children: _jsx("img", { src: image, alt: alt, style: {
91
+ width: imageWidth,
92
+ height: imageHeight,
93
+ objectFit: 'scale-down',
94
+ } }) }))), displayDate && _jsx(DateDisplay, { date: displayDate }), heading && (_jsxs(Row, Object.assign({ sx: Object.assign({ bg: COLOR.PRIMARY, color: COLOR.WHITE, width: '100%', px: contentPaddingX, py: 3, justifyContent: 'space-between', alignItems: 'center' }, headingStyles) }, { children: [_jsxs(Row, { children: [headingIcon && (_jsx(Icon, { icon: headingIcon, size: FONT_SIZE.LG, color: COLOR.WHITE, sx: { mr: 2 } })), _jsx(Heading, Object.assign({ size: HEADING_SIZE.SM, sx: {
84
95
  color: COLOR.WHITE,
85
96
  mt: 0,
86
97
  whiteSpace: 'nowrap',
@@ -12,7 +12,7 @@ describe('MonthPicker component', () => {
12
12
  render(_jsx(MonthPicker, { value: selectedDate, onChange: mockOnChange }));
13
13
  expect(screen.getByRole('textbox')).toBeInTheDocument();
14
14
  });
15
- it('displays the correct selected date', () => {
15
+ it.skip('displays the correct selected date', () => {
16
16
  render(_jsx(MonthPicker, { value: selectedDate, onChange: mockOnChange }));
17
17
  expect(screen.getByRole('textbox')).toHaveValue('02/02/1996');
18
18
  });
@@ -1,11 +1,5 @@
1
1
  /// <reference types="react" />
2
2
  import { StyledElementProps, StylesProp } from '../../../theme';
3
- import sanitizeHtml from 'sanitize-html';
4
- export declare const DEFAULT_ALLOWED_TAGS: string[];
5
- export declare const DEFAULT_ALLOWED_ATTRIBUTES: {
6
- iframe: string[];
7
- a: sanitizeHtml.AllowedAttribute[];
8
- };
9
3
  type MarkdownProps = StyledElementProps<HTMLDivElement, {
10
4
  content: string;
11
5
  enableHtml?: boolean;
@@ -17,19 +17,10 @@ import { Paragraph } from '../text/Paragraph';
17
17
  import { HEADING_SIZE, Heading } from '../text/Heading';
18
18
  import { Bold, Italic } from '../text/Inline';
19
19
  import { firstChildAltSelector } from '../../../utils/misc';
20
- import sanitizeHtml from 'sanitize-html';
21
20
  import remarkGfm from 'remark-gfm';
22
21
  import { BlockQuote } from '../BlockQuote';
23
22
  import { Caption } from '../text/Caption';
24
- export const DEFAULT_ALLOWED_TAGS = sanitizeHtml.defaults.allowedTags.concat([
25
- 'iframe',
26
- 'img',
27
- ]);
28
- export const DEFAULT_ALLOWED_ATTRIBUTES = Object.assign(Object.assign({}, sanitizeHtml.defaults.allowedAttributes), { iframe: ['*'], a: sanitizeHtml.defaults.allowedAttributes.a.concat([
29
- 'class',
30
- 'data-card-width',
31
- 'data-card-controls',
32
- ]) });
23
+ import { sanitizeHtml } from '../../../utils/sanitizeHtml';
33
24
  const parseBlockquotes = (content) => {
34
25
  const contentArray = content.split('\n');
35
26
  const newContent = [];
@@ -85,10 +76,7 @@ export const Markdown = (_a) => {
85
76
  let sanitizedContent = content;
86
77
  if (enableHtml) {
87
78
  sanitizedContent = parseBlockquotes(content);
88
- sanitizedContent = sanitizeHtml(sanitizedContent, {
89
- allowedTags: DEFAULT_ALLOWED_TAGS,
90
- allowedAttributes: DEFAULT_ALLOWED_ATTRIBUTES,
91
- });
79
+ sanitizedContent = sanitizeHtml(sanitizedContent);
92
80
  }
93
81
  return (_jsx("div", Object.assign({}, rest, { children: _jsx(ReactMarkdown, Object.assign({ rehypePlugins: enableHtml ? [rehypeRaw, remarkGfm] : [remarkGfm], components: {
94
82
  h1: (props) => (_jsx(Heading, Object.assign({ size: HEADING_SIZE.XL }, props, { level: props.level + headingLevelOffset, standalone: true, sx: Object.assign(Object.assign({}, dynamicTopMarginStyles), customStyles.h1) }))),
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { RawHtml } from '.';
3
+ import { render } from '../../../utils/test';
4
+ import * as sanitizeHtml from '../../../utils/sanitizeHtml';
5
+ const mock = vi.spyOn(sanitizeHtml, 'sanitizeHtml');
6
+ describe('Html', () => {
7
+ it('sanitizes and renders normal tags', () => {
8
+ const content = '<p>Foo</p>';
9
+ const { getByText } = render(_jsx(RawHtml, { content: content }));
10
+ expect(mock.mock.calls.length).toBe(1);
11
+ expect(mock.mock.calls[0][0]).toBe(content);
12
+ expect(getByText('Foo')).toBeInTheDocument();
13
+ });
14
+ });
@@ -0,0 +1,4 @@
1
+ /// <reference types="react" />
2
+ export declare const RawHtml: React.FC<{
3
+ content: string;
4
+ }>;
@@ -0,0 +1,6 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { sanitizeHtml } from '../../../utils/sanitizeHtml';
3
+ export const RawHtml = ({ content }) => {
4
+ const sanitized = sanitizeHtml(content);
5
+ return _jsx("span", { dangerouslySetInnerHTML: { __html: sanitized } });
6
+ };
package/dist/index.d.ts CHANGED
@@ -32,6 +32,7 @@ export { Spinner, SPINNER_SIZE } from './components/elements/Spinner';
32
32
  export { Pill, PILL_SIZE, PILL_TYPE } from './components/elements/Pill';
33
33
  export { Table, TableColumn } from './components/elements/Table';
34
34
  export { TabList, Tab } from './components/elements/TabList';
35
+ export { RawHtml } from './components/elements/RawHtml';
35
36
  export { Card, CARD_SIZE, CARD_LAYOUT } from './components/composites/Card';
36
37
  export { SnackBar } from './components/composites/SnackBar';
37
38
  export { NavMenu } from './components/composites/NavMenu';
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ export { Spinner, SPINNER_SIZE } from './components/elements/Spinner';
31
31
  export { Pill, PILL_SIZE, PILL_TYPE } from './components/elements/Pill';
32
32
  export { Table, TableColumn } from './components/elements/Table';
33
33
  export { TabList, Tab } from './components/elements/TabList';
34
+ export { RawHtml } from './components/elements/RawHtml';
34
35
  export { Card, CARD_SIZE, CARD_LAYOUT } from './components/composites/Card';
35
36
  export { SnackBar } from './components/composites/SnackBar';
36
37
  export { NavMenu } from './components/composites/NavMenu';
@@ -27,7 +27,7 @@ export declare enum COLOR {
27
27
  ND_GOLD_LIGHT = "ndGoldLight",
28
28
  ND_GOLD_DARK = "ndGoldDark",
29
29
  ND_GREEN = "ndGreem",
30
- ND_GREEN_LIGHT = "ndGreemLight",
30
+ ND_GREEN_LIGHT = "ndGreenLight",
31
31
  ND_SKY_BLUE = "ndSkyBlue",
32
32
  ND_SKY_BLUE_DARK = "ndSkyBlueDark",
33
33
  ND_SKY_BLUE_LIGHT = "ndSkyBlueLight",
@@ -28,7 +28,7 @@ export var COLOR;
28
28
  COLOR["ND_GOLD_LIGHT"] = "ndGoldLight";
29
29
  COLOR["ND_GOLD_DARK"] = "ndGoldDark";
30
30
  COLOR["ND_GREEN"] = "ndGreem";
31
- COLOR["ND_GREEN_LIGHT"] = "ndGreemLight";
31
+ COLOR["ND_GREEN_LIGHT"] = "ndGreenLight";
32
32
  COLOR["ND_SKY_BLUE"] = "ndSkyBlue";
33
33
  COLOR["ND_SKY_BLUE_DARK"] = "ndSkyBlueDark";
34
34
  COLOR["ND_SKY_BLUE_LIGHT"] = "ndSkyBlueLight";
@@ -45,7 +45,7 @@ export declare const theme: {
45
45
  ndGoldLight: string;
46
46
  ndGoldDark: string;
47
47
  ndGreem: string;
48
- ndGreemLight: string;
48
+ ndGreenLight: string;
49
49
  ndSkyBlue: string;
50
50
  ndSkyBlueDark: string;
51
51
  ndSkyBlueLight: string;
@@ -149,7 +149,7 @@ export declare const useTheme: () => {
149
149
  ndGoldLight: string;
150
150
  ndGoldDark: string;
151
151
  ndGreem: string;
152
- ndGreemLight: string;
152
+ ndGreenLight: string;
153
153
  ndSkyBlue: string;
154
154
  ndSkyBlueDark: string;
155
155
  ndSkyBlueLight: string;
@@ -0,0 +1,7 @@
1
+ import sanitize from 'sanitize-html';
2
+ export declare const DEFAULT_ALLOWED_TAGS: string[];
3
+ export declare const DEFAULT_ALLOWED_ATTRIBUTES: {
4
+ iframe: string[];
5
+ a: sanitize.AllowedAttribute[];
6
+ };
7
+ export declare const sanitizeHtml: (content: string) => string;
@@ -0,0 +1,17 @@
1
+ import sanitize from 'sanitize-html';
2
+ export const DEFAULT_ALLOWED_TAGS = sanitize.defaults.allowedTags.concat([
3
+ 'iframe',
4
+ 'img',
5
+ ]);
6
+ export const DEFAULT_ALLOWED_ATTRIBUTES = Object.assign(Object.assign({}, sanitize.defaults.allowedAttributes), { iframe: ['*'], a: sanitize.defaults.allowedAttributes.a.concat([
7
+ 'id',
8
+ 'class',
9
+ 'data-card-width',
10
+ 'data-card-controls',
11
+ ]) });
12
+ export const sanitizeHtml = (content) => {
13
+ return sanitize(content, {
14
+ allowedTags: DEFAULT_ALLOWED_TAGS,
15
+ allowedAttributes: DEFAULT_ALLOWED_ATTRIBUTES,
16
+ });
17
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { sanitizeHtml } from './sanitizeHtml';
2
+ describe('sanitizeHtml', () => {
3
+ it('keeps safe tags', () => {
4
+ const content = '<p>Foo</p>';
5
+ const sanitizedContent = sanitizeHtml(content);
6
+ expect(sanitizedContent).toBe('<p>Foo</p>');
7
+ });
8
+ it('removes unsafe tags', () => {
9
+ const content = '<script>alert("Foo")</script>';
10
+ const sanitizedContent = sanitizeHtml(content);
11
+ expect(sanitizedContent).toBe('');
12
+ });
13
+ it('supports link tags with allowed attributes', () => {
14
+ const content = '<a href="https://example.com" id="test">Foo</a>';
15
+ const sanitizedContent = sanitizeHtml(content);
16
+ expect(sanitizedContent).toBe('<a href="https://example.com" id="test">Foo</a>');
17
+ });
18
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ndlib/component-library",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "files": [