@hyphen/hyphen-components 6.12.0 → 6.14.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyphen/hyphen-components",
3
- "version": "6.12.0",
3
+ "version": "6.14.0",
4
4
  "license": "MIT",
5
5
  "author": {
6
6
  "name": "@hyphen"
@@ -86,7 +86,7 @@
86
86
  },
87
87
  "devDependencies": {
88
88
  "@babel/core": "^7.27.4",
89
- "@chromatic-com/storybook": "^4.1.1",
89
+ "@chromatic-com/storybook": "^4.1.3",
90
90
  "@semantic-release/commit-analyzer": "^11.1.0",
91
91
  "@size-limit/preset-small-lib": "^11.2.0",
92
92
  "@storybook/addon-a11y": "^9.1.3",
@@ -105,7 +105,7 @@
105
105
  "@types/react-modal": "^3.16.3",
106
106
  "autoprefixer": "^10.4.20",
107
107
  "babel-loader": "^9.1.3",
108
- "chromatic": "^13.1.3",
108
+ "chromatic": "^13.3.4",
109
109
  "clean-webpack-plugin": "^4.0.0",
110
110
  "cross-env": "^7.0.3",
111
111
  "css-loader": "^6.11.0",
@@ -1,7 +1,6 @@
1
1
  import { IconName } from '../../types';
2
2
  import { AlertVariant } from './Alert.types';
3
3
 
4
- // eslint-disable-next-line import/prefer-default-export
5
4
  export const ALERT_VARIANTS: AlertVariant[] = [
6
5
  'info',
7
6
  'success',
@@ -1,30 +1,96 @@
1
1
  import { Alert } from './Alert';
2
2
  import { AlertVariant } from './Alert.types';
3
3
  import { Button } from '../Button/Button';
4
+ import { ALERT_VARIANTS } from './Alert.constants';
4
5
 
5
- import type { Meta } from '@storybook/react-vite';
6
+ import type { Meta, StoryObj } from '@storybook/react-vite';
6
7
  import React from 'react';
7
8
  import { useState } from 'react';
8
9
 
9
10
  const meta: Meta<typeof Alert> = {
10
11
  title: 'Components/Alert',
11
12
  component: Alert,
13
+ argTypes: {
14
+ variant: {
15
+ control: 'select',
16
+ options: ALERT_VARIANTS,
17
+ description: 'The type/color of the alert to show',
18
+ },
19
+ title: {
20
+ control: 'text',
21
+ description: 'The title for the alert',
22
+ },
23
+ message: {
24
+ control: 'text',
25
+ description:
26
+ 'The text message to be rendered in the alert (deprecated, use children instead)',
27
+ },
28
+ hasIcon: {
29
+ control: 'boolean',
30
+ description:
31
+ 'Whether the alert has an icon that corresponds to its variant',
32
+ },
33
+ isCompact: {
34
+ control: 'boolean',
35
+ description: 'Renders a version of the alert with smaller padding',
36
+ },
37
+ className: {
38
+ control: 'text',
39
+ description: 'Custom class to apply to the alert',
40
+ },
41
+ onClose: {
42
+ action: 'closed',
43
+ description: 'Callback when alert is closed',
44
+ },
45
+ },
46
+ args: {
47
+ variant: 'default',
48
+ hasIcon: false,
49
+ isCompact: false,
50
+ },
12
51
  };
13
52
 
14
53
  export default meta;
54
+ type Story = StoryObj<typeof Alert>;
15
55
 
16
- export const Overview = () => (
17
- <Alert
18
- title="Contact Created"
19
- message="The contact was saved on December 3, 2020 at 6:10pm PDT"
20
- variant="success"
21
- isClosable
22
- hasIcon
23
- />
24
- );
56
+ export const Overview: Story = {
57
+ args: {
58
+ title: 'Contact Created',
59
+ variant: 'success',
60
+ hasIcon: true,
61
+ children: 'The contact was saved on December 3, 2020 at 6:10pm PDT',
62
+ },
63
+ };
64
+
65
+ export const Compact: Story = {
66
+ args: {
67
+ title: 'Contact Created',
68
+ variant: 'success',
69
+ hasIcon: true,
70
+ isCompact: true,
71
+ children: 'The contact was saved on December 3, 2020 at 6:10pm PDT',
72
+ },
73
+ };
74
+
75
+ export const WithCustomContent: Story = {
76
+ args: {
77
+ title: 'Custom Content',
78
+ variant: 'info',
79
+ hasIcon: true,
80
+ },
81
+ render: (args) => (
82
+ <Alert {...args}>
83
+ <p>
84
+ This alert uses <strong>children</strong> for custom content. You can
85
+ include any JSX content here, including <a href="/#">links</a>,{' '}
86
+ <code>code snippets</code>, and more.
87
+ </p>
88
+ </Alert>
89
+ ),
90
+ };
25
91
 
26
- export const Variants = () =>
27
- (() => {
92
+ export const Variants: Story = {
93
+ render: () => {
28
94
  const variants: AlertVariant[] = [
29
95
  'default',
30
96
  'info',
@@ -41,62 +107,51 @@ export const Variants = () =>
41
107
  <>
42
108
  {variants.map((variant: AlertVariant) => (
43
109
  <Alert
44
- message={message(variant)}
45
110
  key={variant}
46
111
  title={variant.charAt(0).toUpperCase() + variant.slice(1)}
47
112
  variant={variant}
48
113
  hasIcon
49
114
  className="m-bottom-md"
50
- />
115
+ >
116
+ {message(variant)}
117
+ </Alert>
51
118
  ))}
52
119
  </>
53
120
  );
54
- })();
121
+ },
122
+ parameters: {
123
+ controls: { disable: true },
124
+ },
125
+ };
55
126
 
56
- export const Closable = () => {
57
- const [isAlertTwoShowing, setAlertTwoShowing] = useState(true);
58
- const [isAlertThreeShowing, setAlertThreeShowing] = useState(true);
127
+ export const ClosableAlert = () => {
128
+ const [isAlertShowing, setIsAlertShowing] = useState(true);
59
129
 
60
130
  return (
61
131
  <>
62
- <Alert
63
- title="Won't Close"
64
- message="Closable, but with no onClose callback so nothing happens when clicked."
65
- variant="warning"
66
- isClosable
67
- className="m-bottom-md"
68
- />
69
- {isAlertTwoShowing ? (
132
+ {isAlertShowing ? (
70
133
  <Alert
71
134
  title="Closable"
72
- message="This one works!"
73
135
  variant="info"
74
- isClosable
75
- onClose={() => setAlertTwoShowing(false)}
136
+ onClose={() => setIsAlertShowing(false)}
76
137
  className="m-bottom-md"
77
- />
138
+ >
139
+ Try closing me.
140
+ </Alert>
78
141
  ) : (
79
142
  <div className="m-bottom-md">
80
- <Button onClick={() => setAlertTwoShowing(true)} size="sm">
81
- Give me the second alert back!
82
- </Button>
83
- </div>
84
- )}
85
- {isAlertThreeShowing ? (
86
- <Alert
87
- message="With custom close text!"
88
- variant="info"
89
- isClosable
90
- onClose={() => setAlertThreeShowing(false)}
91
- closeText="Close me!"
92
- />
93
- ) : (
94
- <div className="m-bottom-md">
95
- <Button onClick={() => setAlertThreeShowing(true)} size="sm">
96
- Give me the third alert back!
143
+ <Button onClick={() => setIsAlertShowing(true)} size="sm">
144
+ Give me the alert back!
97
145
  </Button>
98
146
  </div>
99
147
  )}
100
148
  </>
101
149
  );
102
150
  };
151
+
152
+ export const Closable: Story = {
153
+ render: () => ClosableAlert(),
154
+ parameters: {
155
+ controls: { disable: true },
156
+ },
157
+ };
@@ -1,5 +1,5 @@
1
1
  import React, { ReactNode } from 'react';
2
- import { render, screen, fireEvent } from '@testing-library/react';
2
+ import { render, screen } from '@testing-library/react';
3
3
  import { Alert } from './Alert';
4
4
  import { ALERT_VARIANTS } from './Alert.constants';
5
5
 
@@ -111,77 +111,13 @@ describe('Alert', () => {
111
111
  });
112
112
  });
113
113
 
114
- describe('Closable Alert', () => {
115
- test('It renders a close icon if `isClosable` prop is passed', () => {
116
- const message = 'I am closable!';
117
- const { rerender } = render(<Alert message={message} />);
118
-
119
- const noCloseButton = screen.queryByTestId('alert-close-icon-test-id');
120
- expect(noCloseButton).not.toBeInTheDocument();
121
-
122
- rerender(<Alert message={message} isClosable />);
123
- const closeButton = screen.queryByTestId('alert-close-icon-test-id');
124
- expect(closeButton).toBeInTheDocument();
125
- });
126
-
127
- test('It renders with custom close text if closeText prop is passed', () => {
128
- const message = 'I am closable too!';
129
- render(<Alert message={message} isClosable closeText="Close me!" />);
130
-
131
- const closeButton = screen.queryByText('Close me!');
132
- expect(closeButton).toBeInTheDocument();
133
- });
134
-
135
- test('It fires a callback if onClose prop is passed', () => {
136
- const message = 'I am closable too!';
137
- const mockOnClose = jest.fn();
138
-
139
- const { rerender } = render(
140
- <Alert message={message} isClosable onClose={mockOnClose} />
141
- );
142
-
143
- const closeButton = screen.queryByTestId('alert-close-icon-test-id');
144
- if (closeButton) fireEvent.click(closeButton);
145
- expect(mockOnClose).toBeCalledTimes(1);
146
- mockOnClose.mockReset();
147
-
148
- rerender(
149
- <Alert
150
- message={message}
151
- isClosable
152
- onClose={mockOnClose}
153
- closeText="close"
154
- />
155
- );
156
- const closeButtonSpan = screen.getByText('close');
157
- if (closeButtonSpan) {
158
- fireEvent.click(closeButtonSpan); // 1
159
- fireEvent.keyUp(closeButtonSpan, { keyCode: 13 }); // 2
160
- fireEvent.keyUp(closeButtonSpan, { keyCode: 13 }); // 3
161
- fireEvent.keyUp(closeButtonSpan, { keyCode: 30 }); // No-op
162
- fireEvent.keyUp(closeButtonSpan, { keyCode: 30 }); // No-op
163
- }
164
- expect(mockOnClose).toBeCalledTimes(3);
165
- mockOnClose.mockReset();
166
-
167
- rerender(<Alert message={message} isClosable closeText="close" />);
168
- const closeButtonNotClickable = screen.getByText('close');
169
- if (closeButtonNotClickable) {
170
- fireEvent.click(closeButtonSpan); // No-op
171
- fireEvent.keyUp(closeButtonSpan, { keyCode: 13 }); // No-op
172
- fireEvent.keyUp(closeButtonSpan, { keyCode: 30 }); // No-op
173
- }
174
- expect(mockOnClose).toBeCalledTimes(0);
175
- });
176
- });
177
-
178
114
  describe('Compact', () => {
179
115
  test('It renders with the compact class when isCompact prop is true', () => {
180
116
  const message = 'Hello world!';
181
117
  render(<Alert message={message} isCompact />);
182
118
 
183
119
  const alert = screen.getByRole('alert');
184
- expect(alert).toHaveClass('p-xs');
120
+ expect(alert).toHaveClass('p-md');
185
121
  });
186
122
  });
187
123
  });
@@ -1,4 +1,12 @@
1
- import React, { FC, ReactNode, MouseEvent, KeyboardEvent } from 'react';
1
+ import React, {
2
+ FC,
3
+ ReactNode,
4
+ MouseEvent,
5
+ KeyboardEvent,
6
+ useCallback,
7
+ useMemo,
8
+ memo,
9
+ } from 'react';
2
10
  import classNames from 'classnames';
3
11
  import { Box } from '../Box/Box';
4
12
  import { Icon } from '../Icon/Icon';
@@ -11,25 +19,16 @@ export interface AlertProps {
11
19
  * Custom class to apply to the alert.
12
20
  */
13
21
  className?: string;
14
- /**
15
- * Custom text to use as a close button.
16
- */
17
- closeText?: string;
18
22
  /**
19
23
  * Whether the alert as an icon that corresponds to its variant (Success, warning, etc.).
20
24
  */
21
25
  hasIcon?: boolean;
22
- /**
23
- * Whether the alert can be closed by the user. If `true` it will render
24
- * the 'close' icon on the right hand side of the alert.
25
- */
26
- isClosable?: boolean;
27
26
  /**
28
27
  * Renders a version of the alert with smaller padding.
29
28
  */
30
29
  isCompact?: boolean;
31
30
  /**
32
- * The text message or ReactNode to be rendered in the alert.
31
+ * @deprecated Use children instead. The text message or ReactNode to be rendered in the alert.
33
32
  */
34
33
  message?: string | ReactNode;
35
34
  /**
@@ -56,12 +55,12 @@ export interface AlertProps {
56
55
  */
57
56
  [x: string]: any; // eslint-disable-line
58
57
  }
59
- export const Alert: FC<AlertProps> = ({
58
+
59
+ const AlertComponent: FC<AlertProps> = ({
60
+ children,
60
61
  className = '',
61
- closeText = '',
62
62
  hasIcon = false,
63
63
  isCompact = false,
64
- isClosable = false,
65
64
  message = '',
66
65
  onClose = undefined,
67
66
  render = undefined,
@@ -69,29 +68,44 @@ export const Alert: FC<AlertProps> = ({
69
68
  variant = 'default',
70
69
  ...restProps
71
70
  }) => {
72
- const handleClose = (
73
- event: MouseEvent<HTMLOrSVGElement> | KeyboardEvent<HTMLSpanElement>
74
- ): void => {
75
- if (!onClose) return;
76
-
77
- onClose(event);
78
- };
71
+ const handleClose = useCallback(
72
+ (
73
+ event: MouseEvent<HTMLOrSVGElement> | KeyboardEvent<HTMLSpanElement>
74
+ ): void => {
75
+ if (!onClose) return;
79
76
 
80
- const renderAlertIcon = (): ReactNode => (
81
- <Box fontSize="md" className={styles[`alert__icon__${variant}`]}>
82
- <Icon
83
- name={ALERT_ICONS_MAP[variant].icon}
84
- data-testid={`alert-icon-${variant}-test-id`}
85
- />
86
- </Box>
77
+ onClose(event);
78
+ },
79
+ [onClose]
87
80
  );
88
81
 
89
- const renderCloseIcon = (): ReactNode => {
90
- const handleCloseKeyPress = (
91
- event: KeyboardEvent<HTMLSpanElement>
92
- ): void => {
82
+ const handleCloseKeyPress = useCallback(
83
+ (event: KeyboardEvent<HTMLSpanElement>): void => {
93
84
  if (event.keyCode === 13) handleClose(event);
94
- };
85
+ },
86
+ [handleClose]
87
+ );
88
+
89
+ const alertContainerClasses = useMemo(
90
+ () => classNames(styles[`alert__${variant}`], styles.alert, className),
91
+ [variant, className]
92
+ );
93
+
94
+ const alertIcon = useMemo(() => {
95
+ if (!hasIcon) return null;
96
+
97
+ return (
98
+ <Box fontSize="md" className={styles[`alert__icon__${variant}`]}>
99
+ <Icon
100
+ name={ALERT_ICONS_MAP[variant].icon}
101
+ data-testid={`alert-icon-${variant}-test-id`}
102
+ />
103
+ </Box>
104
+ );
105
+ }, [hasIcon, variant]);
106
+
107
+ const closeIcon = useMemo(() => {
108
+ if (!onClose) return null;
95
109
 
96
110
  return (
97
111
  <Box
@@ -105,19 +119,35 @@ export const Alert: FC<AlertProps> = ({
105
119
  onKeyUp={handleCloseKeyPress}
106
120
  aria-label="dismiss"
107
121
  >
108
- {closeText || (
109
- <Icon name="remove" data-testid="alert-close-icon-test-id" />
110
- )}
122
+ <Icon name="remove" data-testid="alert-close-icon-test-id" />
111
123
  </button>
112
124
  </Box>
113
125
  );
114
- };
126
+ }, [onClose, handleClose, handleCloseKeyPress]);
115
127
 
116
- const alertContainerClasses: string = classNames(
117
- styles[`alert__${variant}`],
118
- styles.alert,
119
- className
120
- );
128
+ const content = useMemo(() => {
129
+ if (render) {
130
+ return render();
131
+ }
132
+
133
+ return (
134
+ <Box display="block" childGap={message && title ? '2xs' : undefined}>
135
+ {title && (
136
+ <Box
137
+ as="h4"
138
+ fontSize="sm"
139
+ fontWeight="semibold"
140
+ className={styles['alert-heading']}
141
+ >
142
+ {title}
143
+ </Box>
144
+ )}
145
+ {children ??
146
+ (message &&
147
+ (typeof message === 'string' ? <p>{message}</p> : message))}
148
+ </Box>
149
+ );
150
+ }, [render, message, title, children]);
121
151
 
122
152
  return (
123
153
  <Box
@@ -125,34 +155,17 @@ export const Alert: FC<AlertProps> = ({
125
155
  gap="md"
126
156
  className={alertContainerClasses}
127
157
  direction="row"
128
- padding={isCompact ? 'xs' : 'md'}
158
+ padding={isCompact ? 'md' : '2xl'}
129
159
  radius="md"
130
160
  role="alert"
131
161
  fontSize="sm"
132
162
  {...restProps}
133
163
  >
134
- {hasIcon && renderAlertIcon()}
135
- <div>
136
- {render ? (
137
- render()
138
- ) : (
139
- <Box display="block" childGap={message && title ? '2xs' : undefined}>
140
- {title && (
141
- <Box
142
- as="h4"
143
- fontSize="sm"
144
- fontWeight="semibold"
145
- className={styles['alert-heading']}
146
- >
147
- {title}
148
- </Box>
149
- )}
150
- {message &&
151
- (typeof message === 'string' ? <p>{message}</p> : message)}
152
- </Box>
153
- )}
154
- </div>
155
- {isClosable && renderCloseIcon()}
164
+ {alertIcon}
165
+ <div>{content}</div>
166
+ {closeIcon}
156
167
  </Box>
157
168
  );
158
169
  };
170
+
171
+ export const Alert = memo(AlertComponent);
@@ -1,42 +1,86 @@
1
- import { Meta } from '@storybook/react-vite';
2
1
  import { Badge, BadgeVariant } from './Badge';
3
2
  import React from 'react';
4
3
  import { Box } from '../Box/Box';
4
+ import type { Meta, StoryObj } from '@storybook/react-vite';
5
+
6
+ const BADGE_VARIANTS: BadgeVariant[] = [
7
+ 'light-grey',
8
+ 'dark-grey',
9
+ 'inverse',
10
+ 'purple',
11
+ 'blue',
12
+ 'green',
13
+ 'yellow',
14
+ 'red',
15
+ 'hyphen',
16
+ ];
17
+
18
+ const BADGE_SIZES = ['sm', 'md', 'lg'];
5
19
 
6
20
  const meta: Meta<typeof Badge> = {
7
21
  title: 'Components/Badge',
8
22
  component: Badge,
23
+ argTypes: {
24
+ variant: {
25
+ control: 'select',
26
+ options: BADGE_VARIANTS,
27
+ description: 'The type/color of the badge to show',
28
+ },
29
+ size: {
30
+ control: 'select',
31
+ options: BADGE_SIZES,
32
+ description: 'The size of the badge',
33
+ },
34
+ message: {
35
+ control: 'text',
36
+ description:
37
+ 'The text message to be rendered in the badge (deprecated, use children instead)',
38
+ },
39
+ className: {
40
+ control: 'text',
41
+ description: 'Custom class to apply to the badge',
42
+ },
43
+ children: {
44
+ control: 'text',
45
+ description: 'Badge content (preferred over message)',
46
+ },
47
+ },
48
+ args: {
49
+ variant: 'light-grey',
50
+ size: 'md',
51
+ message: '',
52
+ className: '',
53
+ children: undefined,
54
+ },
9
55
  };
10
56
 
11
57
  export default meta;
12
58
 
13
- export const Overview = () => <Badge message="Hello world!" />;
14
-
15
- export const Variants = () => {
16
- const variants = [
17
- 'light-grey',
18
- 'dark-grey',
19
- 'inverse',
20
- 'purple',
21
- 'blue',
22
- 'green',
23
- 'yellow',
24
- 'red',
25
- 'hyphen',
26
- ] as BadgeVariant[];
27
- return (
59
+ type Story = StoryObj<typeof Badge>;
60
+
61
+ export const Overview: Story = {
62
+ args: {
63
+ message: 'Hello world!',
64
+ },
65
+ };
66
+
67
+ export const Variants: Story = {
68
+ render: () => (
28
69
  <Box direction="row" gap="sm">
29
- {variants.map((variant) => (
70
+ {BADGE_VARIANTS.map((variant) => (
30
71
  <Badge variant={variant} key={variant}>
31
72
  {variant}
32
73
  </Badge>
33
74
  ))}
34
75
  </Box>
35
- );
76
+ ),
77
+ parameters: {
78
+ controls: { disable: true },
79
+ },
36
80
  };
37
81
 
38
- export const Sizes = () => (
39
- <>
82
+ export const Sizes: Story = {
83
+ render: () => (
40
84
  <Box direction="column" alignItems="flex-start" gap="md">
41
85
  <Badge size="sm" message="Small" />
42
86
  <Badge size="md" message="Medium" />
@@ -51,5 +95,8 @@ export const Sizes = () => (
51
95
  Responsive
52
96
  </Badge>
53
97
  </Box>
54
- </>
55
- );
98
+ ),
99
+ parameters: {
100
+ controls: { disable: true },
101
+ },
102
+ };